Page 1 of 2 (29 posts)

  • talks about »
  • pyqgis

Tags

Last update:
Tue Dec 11 14:05:13 2018

A Django site.

QGIS Planet

Thoughts on “FOSS4G/SOTM Oceania 2018”, and the PyQGIS API improvements which it caused

Last week the first official “FOSS4G/SOTM Oceania” conference was held at Melbourne University. This was a fantastic event, and there’s simply no way I can extend sufficient thanks to all the organisers and volunteers who put this event together. They did a brilliant job, and their efforts are even more impressive considering it was the inaugural event!

Upfront — this is not a recap of the conference (I’m sure someone else is working on a much more detailed write up of the event!), just some musings I’ve had following my experiences assisting Nathan Woodrow deliver an introductory Python for QGIS workshop he put together for the conference. In short, we both found that delivering this workshop to a group of PyQGIS newcomers was a great way for us to identify “pain points” in the PyQGIS API and areas where we need to improve. The good news is that as a direct result of the experiences during this workshop the API has been improved and streamlined! Let’s explore how:

Part of Nathan’s workshop (notes are available here) focused on a hands-on example of creating a custom QGIS “Processing” script. I’ve found that preparing workshops is guaranteed to expose a bunch of rare and tricky software bugs, and this was no exception! Unfortunately the workshop was scheduled just before the QGIS 3.4.2 patch release which fixed these bugs, but at least they’re fixed now and we can move on…

The bulk of Nathan’s example algorithm is contained within the following block (where “distance” is the length of line segments we want to chop our features up into):

for input_feature in enumerate(features):
    geom = feature.geometry().constGet()
    if isinstance(geom, QgsLineString):
        continue
    first_part = geom.geometryN(0)
    start = 0
    end = distance
    length = first_part.length()

    while start < length:
        new_geom = first_part.curveSubstring(start,end)

        output_feature = input_feature
        output_feature.setGeometry(QgsGeometry(new_geom))
        sink.addFeature(output_feature)

        start += distance
        end += distance

There’s a lot here, but really the guts of this algorithm breaks down to one line:

new_geom = first_part.curveSubstring(start,end)

Basically, a new geometry is created for each trimmed section in the output layer by calling the “curveSubstring” method on the input geometry and passing it a start and end distance along the input line. This returns the portion of that input LineString (or CircularString, or CompoundCurve) between those distances. The PyQGIS API nicely hides the details here – you can safely call this one method and be confident that regardless of the input geometry type the result will be correct.

Unfortunately, while calling the “curveSubstring” method is elegant, all the code surrounding this call is not so elegant. As a (mostly) full-time QGIS developer myself, I tend to look over oddities in the API. It’s easy to justify ugly API as just “how it’s always been”, and over time it’s natural to develop a type of blind spot to these issues.

Let’s start with the first ugly part of this code:

geom = input_feature.geometry().constGet()
if isinstance(geom, QgsLineString):
    continue
first_part = geom.geometryN(0)
# chop first_part into sections of desired length
...

This is rather… confusing… logic to follow. Here the script is fetching the geometry of the input feature, checking if it’s a LineString, and if it IS, then it skips that feature and continues to the next. Wait… what? It’s skipping features with LineString geometries?

Well, yes. The algorithm was written specifically for one workshop, which was using a MultiLineString layer as the demo layer. The script takes a huge shortcut here and says “if the input feature isn’t a MultiLineString, ignore it — we only know how to deal with multi-part geometries”. Immediately following this logic there’s a call to geometryN( 0 ), which returns just the first part of the MultiLineString geometry.

There’s two issues here — one is that the script just plain won’t work for LineString inputs, and the second is that it ignores everything BUT the first part in the geometry. While it would be possible to fix the script and add a check for the input geometry type, put in logic to loop over all the parts of a multi-part input, etc, that’s instantly going to add a LOT of complexity or duplicate code here.

Fortunately, this was the perfect excuse to improve the PyQGIS API itself so that this kind of operation is simpler in future! Nathan and I had a debrief/brainstorm after the workshop, and as a result a new “parts iterator” has been implemented and merged to QGIS master. It’ll be available from version 3.6 on. Using the new iterator, we can simplify the script:

geom = input_feature.geometry()
for part in geom.parts():
    # chop part into sections of desired length
    ...

Win! This is simultaneously more readable, more Pythonic, and automatically works for both LineString and MultiLineString inputs (and in the case of MultiLineStrings, we now correctly handle all parts).

Here’s another pain-point. Looking at the block:

new_geom = part.curveSubstring(start,end)
output_feature = input_feature
output_feature.setGeometry(QgsGeometry(new_geom))

At first glance this looks reasonable – we use curveSubstring to get the portion of the curve, then make a copy of the input_feature as output_feature (this ensures that the features output by the algorithm maintain all the attributes from the input features), and finally set the geometry of the output_feature to be the newly calculated curve portion. The ugliness here comes in this line:

output_feature.setGeometry(QgsGeometry(new_geom))

What’s that extra QgsGeometry(…) call doing here? Without getting too sidetracked into the QGIS geometry API internals, QgsFeature.setGeometry requires a QgsGeometry argument, not the QgsAbstractGeometry subclass which is returned by curveSubstring.

This is a prime example of a “paper-cut” style issue in the PyQGIS API. Experienced developers know and understand the reasons behind this, but for newcomers to PyQGIS, it’s an obscure complexity. Fortunately the solution here was simple — and after the workshop Nathan and I added a new overload to QgsFeature.setGeometry which accepts a QgsAbstractGeometry argument. So in QGIS 3.6 this line can be simplified to:

output_feature.setGeometry(new_geom)

Or, if you wanted to make things more concise, you could put the curveSubstring call directly in here:

output_feature = input_feature
output_feature.setGeometry(part.curveSubstring(start,end))

Let’s have a look at the simplified script for QGIS 3.6:

for input_feature in enumerate(features):
    geom = feature.geometry()
    for part in geom.parts():
        start = 0
        end = distance
        length = part.length()

        while start < length:
            output_feature = input_feature
            output_feature.setGeometry(part.curveSubstring(start,end))
            sink.addFeature(output_feature)

            start += distance
            end += distance

This is MUCH nicer, and will be much easier to explain in the next workshop! The good news is that Nathan has more niceness on the way which will further improve the process of writing QGIS Processing script algorithms. You can see some early prototypes of this work here:

So there we go. The process of writing and delivering a workshop helps to look past “API blind spots” and identify the ugly points and traps for those new to the API. As a direct result of this FOSS4G/SOTM Oceania 2018 Workshop, the QGIS 3.6 PyQGIS API will be easier to use, more readable, and less buggy! That’s a win all round!

PyQGIS for non-programmers

If you’re are following me on Twitter, you’ve certainly already read that I’m working on PyQGIS 101 a tutorial to help GIS users to get started with Python programming for QGIS.

I’ve often been asked to recommend Python tutorials for beginners and I’ve been surprised how difficult it can be to find an engaging tutorial for Python 3 that does not assume that the reader already knows all kinds of programming concepts.

It’s been a while since I started programming, but I do teach QGIS and Python programming for QGIS to university students and therefore have some ideas of which concepts are challenging. Nonetheless, it’s well possible that I overlook something that is not self explanatory. If you’re using PyQGIS 101 and find that some points could use further explanations, please leave a comment on the corresponding page.

PyQGIS 101 is a work in progress. I’d appreciate any feedback, particularly from beginners!

New PyQGIS documentation

We are proud to announce our new dedicated documentation of the QGIS Python API (also called PyQGIS) which is now available at https://qgis.org/pyqgis:

While the QGIS API has long been documented, Python developers in the past had to work with the general C++ documentation that wasn’t always straightforward to use. The new PyQGIS documentation presents the API in an accessible pythonic manner.

Of course, creating a good API documentation from source code in an automated way, is not trivial. A key challenge was to automatically create Python bindings files (or SIP files). A custom Perl script known as “sipify” now enables us to automatically integrate the C++ documentation into the Python bindings and keep them up to date. Another challenge was to create the documentation itself using Sphinx. Two detailed reports containing all the technical details of the first and second generation of the documentation are available if you want to learn more about the underlying architecture.

This has been a really important infrastructure project for QGIS that has been made possible by support from our donors and sponsors, as well as the generous in-kind contributions of our community members.

Where's my .qgis3 Folder?

There's been several posts to GIS StackExchange along the lines of:

Where's my .qgis3 folder?

Prior to QGIS 3, the .qgis/.qgis2 folder was found under your home directory. At version 3, the folder has moved to a more standard profile location for your operating system.

There are a couple of ways to determine where the folder is located:

  • Use the Settings->User Profiles->Open active profile folder menu item
  • Use QgsApplication.qgisSettingsDirPath from Python or the console

Here are the "standard" locations for Linux, Mac, and Windows, as found under your HOME directory:

  • Linux:
    • .local/share/QGIS/QGIS3/profiles/default
  • Mac OS X:
    • Library/Application Support/QGIS/QGIS3/profiles/default
  • Windows:
    • AppData\Roaming\QGIS\QGIS3\profiles\default

To get the location of your plugins directory, just add python/plugins to the appropriate location above. For example:

AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins

From the Settings->User Profiles menu, you'll notice a New profile item. This allows you to have multiple configurations of QGIS 3. Each new profile is created in the same "base" location as listed above. For example:

AppData\Roaming\QGIS\QGIS3\profiles\new_profile

Quick Guide to Getting Started with PyQGIS 3 on Windows

Getting started with Python and QGIS 3 can be a bit overwhelming. In this post we give you a quick start to get you up and running and maybe make your PyQGIS life a little easier.

There are likely many ways to setup a working PyQGIS development environment---this one works pretty well.

Contents

Requirements

  • OSGeo4W Advanced Install of QGIS
  • pip (for installing/managing Python packages)
  • pb_tool (cross-platform tool for compiling/deploying/distributing QGIS plugin)
  • A customized startup script to set the environment (pyqgis.cmd)
  • IDE (optional)
  • Emacs (just kidding)
  • Vim (just kidding)

We'll start with the installs.

Installing

Almost everything we need can be installed using the OSGeo4W installer available on the QGIS website.

OSGeo4W

From the QGIS website, download the appropriate network installer (32 or 64 bit) for QGIS 3.

  • Run the installer and choose the Advanced Install option
  • Install from Internet
  • Choose a directory for the install---I prefer a path without spaces such as C:\OSGeo4W
  • Accept default for local package directory and Start menu name
  • Tweak network connection option if needed on the Select Your Internet Connection screen
  • Accept default download site location
  • From the Select packages screen, select: Desktop -> qgis: QGIS Desktop

When you click Next a bunch of additional packages will be suggested---just accept them and continue the install.

Once complete you will have a functioning QGIS install along with the other parts we need. If you want to work with the nightly build of QGIS, choose Desktop -> qgis-dev instead.

If you installed QGIS using the standalone installer, the easiest option is to remove it and install from OSGeo4W. You can run both the standalone and OSGeo4W versions on the same machine, but you need to be extra careful not to mix up the environment.

Setting the Environment

To continue with the setup, we need to set the environment by creating a .cmd script. The following is adapted from several sources, and trimmed down to the minimum. Copy and paste it into a file named pyqgis.cmd and save it to a convenient location (like your HOME directory).

@echo off
SET OSGEO4W_ROOT=C:\OSGeo4W3
call "%OSGEO4W_ROOT%"\bin\o4w_env.bat
call "%OSGEO4W_ROOT%"\apps\grass\grass-7.4.0\etc\env.bat
@echo off
path %PATH%;%OSGEO4W_ROOT%\apps\qgis\bin
path %PATH%;%OSGEO4W_ROOT%\apps\grass\grass-7.4.0\lib
path %PATH%;C:\OSGeo4W3\apps\Qt5\bin
path %PATH%;C:\OSGeo4W3\apps\Python36\Scripts

set PYTHONPATH=%PYTHONPATH%;%OSGEO4W_ROOT%\apps\qgis\python
set PYTHONHOME=%OSGEO4W_ROOT%\apps\Python36

set PATH=C:\Program Files\Git\bin;%PATH%

cmd.exe

You should customize the set PATH statement to add any paths you want available when working from the command line. I added paths to my git install.

The last line starts a cmd shell with the settings specified above it. We'll see an example of starting an IDE in a bit.

You can test to make sure all is well by double-clicking on our pyqgis.cmd script, then starting Python and attempting to import one of the QGIS modules:

C:\Users\gsherman>python3
Python 3.6.0 (v3.6.0:41df79263a11, Dec 23 2016, 07:18:10) [MSC v.1900 32 bit (In tel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import qgis.core
>>> import PyQt5.QtCore

If you don't get any complaints on import, things are looking good.

Installing pb_tool

Open your customized shell (double-click on pyqgis.cmd to start it) to install pb_tool:

python3 -m pip install pb_tool

Check to see if pb_tool is installed correctly:

C:\Users\gsherman>pb_tool
Usage: pb_tool [OPTIONS] COMMAND [ARGS]...

  Simple Python tool to compile and deploy a QGIS plugin. For help on a
  command use --help after the command: pb_tool deploy --help.

  pb_tool requires a configuration file (default: pb_tool.cfg) that declares
  the files and resources used in your plugin. Plugin Builder 2.6.0 creates
  a config file when you generate a new plugin template.

  See http://g-sherman.github.io/plugin_build_tool for for an example config
  file. You can also use the create command to generate a best-guess config
  file for an existing project, then tweak as needed.

  Bugs and enhancement requests, see:
  https://github.com/g-sherman/plugin_build_tool

Options:
  --help  Show this message and exit.

Commands:
  clean       Remove compiled resource and ui files
  clean_docs  Remove the built HTML help files from the...
  compile     Compile the resource and ui files
  config      Create a config file based on source files in...
  create      Create a new plugin in the current directory...
  dclean      Remove the deployed plugin from the...
  deploy      Deploy the plugin to QGIS plugin directory...
  doc         Build HTML version of the help files using...
  help        Open the pb_tools web page in your default...
  list        List the contents of the configuration file
  translate   Build translations using lrelease.
  update      Check for update to pb_tool
  validate    Check the pb_tool.cfg file for mandatory...
  version     Return the version of pb_tool and exit
  zip         Package the plugin into a zip file suitable...

If you get an error, make sure C:\OSGeo4W3\apps\Python36\Scripts is in your PATH.

More information on using pb_tool is available on the project website.

Working on the Command Line

Just double-click on your pyqgis.cmd script from the Explorer or a desktop shortcut to start a cmd shell. From here you can use Python interactively and also use pb_tool to compile and deploy your plugin for testing.

IDE Example

By adding one line to our pyqgis.cmd script, we can start our IDE with the proper settings to recognize the QGIS libraries:

start "PyCharm aware of Quantum GIS" /B "C:\Program Files (x86)\JetBrains\PyCharm 3.4.1\bin\pycharm.exe" %*

We added the start statement with the path to the IDE (in this case PyCharm). If you save this to something like pycharm.cmd, you can double-click on it to start PyCharm. The same method works for other IDEs, such as PyDev.

Within your IDE settings, point it to use the Python interpreter included with OSGeo4W---typically at: %OSGEO4W_ROOT%\bin\python3.exe. This will make it pick up all the QGIS goodies needed for development, completion, and debugging. In my case OSGEO4W_ROOT is C:\OSGeo4W3, so in the IDE, the path to the correct Python interpreter would be: C:\OSGeo4W3\bin\python3.exe.

Make sure you adjust the paths in your .cmd scripts to match your system and software locations.

Workflow

Here is an example of a workflow you can use once you're setup for development.

Creating a New Plugin

  1. Use the Plugin Builder plugin to create a starting point [1]
  2. Start your pyqgis.cmd shell
  3. Use pb_tool to compile and deploy the plugin (pb_tool deploy will do it all in one pass)
  4. Activate it in QGIS and test it out
  5. Add code, deploy, test, repeat

Working with Existing Plugin Code

The steps are basically the same was creating a new plugin, except we start by using pb_tool to create a new config file:

  1. Start your pyqgis.cmd shell
  2. Change to the directory containing your plugin code
  3. Use pb_tool create to create a config file
  4. Edit pb_tool.cfg to adjust/add things create may have missed
  5. Start at step 3 in Creating a New Plugin and press on

Troubleshooting

Assuming you have things properly installed, trouble usually stems from an incorrect environment.

  • Make sure QGIS runs and the Python console is available and working
  • Check all the paths in your pygis.cmd or your custom IDE cmd script
  • Make sure your IDE is using the Python interpreter that comes with OSGeo4W


[1] Plugin Builder 3.x generates a pb_tool config file

Porting Processing scripts to QGIS3

I’ll start with some tech talk first. Feel free to jump to the usage example further down if you are here for the edge bundling plugin.

As you certainly know, QGIS 3 brings a lot of improvements and under-the-hood changes. One of those changes affects all Python scripts. They need to be updated to Python 3 and the new PyQGIS API. (See the official migration guide for details.)

To get ready for the big 3.0 release, I’ve started porting my Processing tools. The edge bundling script is my first candidate for porting to QGIS 3. I also wanted to use this opportunity to “upgrade” from a simple script to a plugin that integrates into Processing.

I used Alexander Bruy’s “prepair for Processing” plugin as a template but you can also find an example template in your Processing folder. (On my system, it is located in C:\OSGeo4W64\apps\qgis-dev\python\plugins\processing\algs\exampleprovider.)

Since I didn’t want to miss the advantages of a good IDE, I set up PyCharm as described by Heikki Vesanto. This will give you code completion for Python 3 and PyQGIS which is very helpful for refactoring and porting. (I also tried Eclipse with PyDev but if you don’t have a favorite IDE yet, I find PyCharm easier to install and configure.)

My PyCharm startup script qgis3_pycharm.bat is a copy of C:\OSGeo4W64\bin\python-qgis-dev.bat with the last line altered to start PyCharm:

@echo off
call "%~dp0\o4w_env.bat"
call qt5_env.bat
call py3_env.bat
@echo off<span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span>
path %OSGEO4W_ROOT%\apps\qgis-dev\bin;%PATH%
set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/qgis-dev
set GDAL_FILENAME_IS_UTF8=YES
rem Set VSI cache to be used as buffer, see #6448
set VSI_CACHE=TRUE
set VSI_CACHE_SIZE=1000000
set QT_PLUGIN_PATH=%OSGEO4W_ROOT%\apps\qgis-dev\qtplugins;%OSGEO4W_ROOT%\apps\qt5\plugins
set PYTHONPATH=%OSGEO4W_ROOT%\apps\qgis-dev\python;%PYTHONPATH%
start /d "C:\Program Files\JetBrains\PyCharm\bin\" pycharm64.exe

In PyCharm File | Settings, I configured the OSGeo4W Python 3.6 interpreter and added qgis-dev and the plugin folder to its path:

With this setup done, we can go back to the code.

I first resolved all occurrences of import * in my script to follow good coding practices. For example:

from qgis.core import *

became

from qgis.core import QgsFeature, QgsPoint, QgsVector, QgsGeometry, QgsField, QGis<span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span>

in this PR.

I didn’t even run the 2to3 script that is provided to make porting from Python 2 to Python 3 easier. Since the edge bundling code is mostly Numpy, there were almost no changes necessary. The only head scratching moment was when Numpy refused to add a map() return value to an array. So (with the help of Stackoverflow of course) I added a work around to convert the map() return value to an array as well:

flocal_x = map(forcecalcx, subtr_x, subtr_y, distance)
electrostaticforces_x[e_idx, :] += np.array(list(flocal_x))

The biggest change related to Processing is that the VectorWriter has been replaced by a QgsFeatureSink. It’s defined as a parameter of the edgebundling QgsProcessingAlgorithm:

self.addParameter(QgsProcessingParameterFeatureSink(
   self.OUTPUT,
   self.tr("Bundled edges"),
   QgsProcessing.TypeVectorLine)
)

And when the algorithm is run, the sink is filled with the output features:

(sink, dest_id) = self.parameterAsSink(
   parameters, self.OUTPUT, context,
   source.fields(), source.wkbType(), source.sourceCrs()
)

# code that creates features

sink.addFeature(feat, QgsFeatureSink.FastInsert)

The ported plugin is available on Github.

The edge bundling plugin in action

I haven’t uploaded the plugin to the official plugin repository yet, but you can already download if from Github and give it a try:

For this example, I’m using taxi pick-up and drop-off data provided by the NYC Taxi & Limousine Commission. I downloaded the January 2017 green taxi data and extracted all trips for the 1st of January. Then I created origin-destination (OD) lines using the QGIS virtual layer feature:

To get an interesting subset of the data, I extracted only those OD flows that cross the East River and have a count of at least 5 taxis:

Now the data is ready for bundling.

If you have installed the edge bundling plugin, the force-directed edge bundling algorithm should be available in the Processing toolbox. The UI of the edge bundling algorithm looks pretty much the same as it did for the QGIS 2 Processing script:

Since this is a small dataset with only 148 OD flows, the edge bundling processes is pretty quick and we can explore the results:

Beyond this core edge bundling algorithm, the repository also contains two more scripts that still need to be ported. They include dependencies on sklearn, so it will be interesting to see how straightforward it is to convert them.

Cours PyQGIS 13.11./14.11.2017 à Neuchâtel

Le cours est complet. Le cours est destiné aux utilisateurs avancés de QGIS qui souhaitent accroître leurs possibilités grâce à l’utilisation de python dans QGIS. Lors de cette formation, nous aborderons différentes possibilités d’interaction avec l’API QGIS ainsi que la

Movement data in GIS #3: visualizing massive trajectory datasets

In the fist two parts of the Movement Data in GIS series, I discussed modeling trajectories as LinestringM features in PostGIS to overcome some common issues of movement data in GIS and presented a way to efficiently render speed changes along a trajectory in QGIS without having to split the trajectory into shorter segments.

While visualizing individual trajectories is important, the real challenge is trying to visualize massive trajectory datasets in a way that enables further analysis. The out-of-the-box functionality of GIS is painfully limited. Except for some transparency and heatmap approaches, there is not much that can be done to help interpret “hairballs” of trajectories. Luckily researchers in visual analytics have already put considerable effort into finding solutions for this visualization challenge. The approach I want to talk about today is by Andrienko, N., & Andrienko, G. (2011). Spatial generalization and aggregation of massive movement data. IEEE Transactions on visualization and computer graphics, 17(2), 205-219. and consists of the following main steps:

  1. Extracting characteristic points from the trajectories
  2. Grouping the extracted points by spatial proximity
  3. Computing group centroids and corresponding Voronoi cells
  4. Deviding trajectories into segments according to the Voronoi cells
  5. Counting transitions from one cell to another

The authors do a great job at describing the concepts and algorithms, which made it relatively straightforward to implement them in QGIS Processing. So far, I’ve implemented the basic logic but the paper contains further suggestions for improvements. This was also my first pyQGIS project that makes use of the measurement value support in the new geometry engine. The time information stored in the m-values is used to detect stop points, which – together with start, end, and turning points – make up the characteristic points of a trajectory.

The following animation illustrates the current state of the implementation: First the “hairball” of trajectories is rendered. Then we extract the characteristic points and group them by proximity. The big black dots are the resulting group centroids. From there, I skipped the Voronoi cells and directly counted transitions from “nearest to centroid A” to “nearest to centroid B”.

(data credits: GeoLife project)

From thousands of individual trajectories to a generalized representation of overall movement patterns (data credits: GeoLife project, map tiles: Stamen, map data: OSM)

The resulting visualization makes it possible to analyze flow strength as well as directionality. I have deliberately excluded all connections with a count below 10 transitions to reduce visual clutter. The cell size / distance between point groups – and therefore the level-of-detail – is one of the input parameters. In my example, I used a target cell size of approximately 2km. This setting results in connections which follow the major roads outside the city center very well. In the city center, where the road grid is tighter, trajectories on different roads mix and the connections are less clear.

Since trajectories in this dataset are not limited to car trips, it is expected to find additional movement that is not restricted to the road network. This is particularly noticeable in the dense area in the west where many slow trajectories – most likely from walking trips – are located. The paper also covers how to ensure that connections are limited to neighboring cells by densifying the trajectories before computing step 4.

trajectory_generalization

Running the scripts for over 18,000 trajectories requires patience. It would be worth evaluating if the first three steps can be run with only a subsample of the data without impacting the results in a negative way.

One thing I’m not satisfied with yet is the way to specify the target cell size. While it’s possible to measure ellipsoidal distances in meters using QgsDistanceArea (irrespective of the trajectory layer’s CRS), the initial regular grid used in step 2 in order to group the extracted points has to be specified in the trajectory layer’s CRS units – quite likely degrees. Instead, it may be best to transform everything into an equidistant projection before running any calculations.

It’s good to see that PyQGIS enables us to use the information encoded in PostGIS LinestringM features to perform spatio-temporal analysis. However, working with m or z values involves a lot of v2 geometry classes which work slightly differently than their v1 counterparts. It certainly takes some getting used to. This situation might get cleaned up as part of the QGIS 3 API refactoring effort. If you can, please support work on QGIS 3. Now is the time to shape the PyQGIS API for the following years!


Speeding up your PyQGIS scripts

I’ve recently spent some time optimising the performance of various QGIS plugins and algorithms, and I’ve noticed that there’s a few common performance traps which developers fall into when fetching features from a vector layer. In this post I’m going to explore these traps, what makes them slow, and how to avoid them.

As a bit of background, features are fetched from a vector layer in QGIS using a QgsFeatureRequest object. Common use is something like this:

request = QgsFeatureRequest()
for feature in vector_layer.getFeatures(request):
    # do something

This code would iterate over all the features in layer. Filtering the features is done by tweaking the QgsFeatureRequest, such as:

request = QgsFeatureRequest().setFilterFid(1001)
feature_1001 = next(vector_layer.getFeatures(request))

In this case calling getFeatures(request) just returns the single feature with an ID of 1001 (which is why we shortcut and use next(…) here instead of iterating over the results).

Now, here’s the trap: calling getFeatures is expensive. If you call it on a vector layer, QGIS will be required to setup an new connection to the data store (the layer provider), create some query to return data, and parse each result as it is returned from the provider. This can be slow, especially if you’re working with some type of remote layer, such as a PostGIS table over a VPN connection. This brings us to our first trap:

Trap #1: Minimise the calls to getFeatures()

A common task in PyQGIS code is to take a list of feature IDs and then request those features from the layer. A see a lot of older code which does this using something like:

for id in some_list_of_feature_ids:
    request = QgsFeatureRequest().setFilterFid(id)
    feature = next(vector_layer.getFeatures(request))
    # do something with the feature

Why is this a bad idea? Well, remember that every time you call getFeatures() QGIS needs to do a whole bunch of things before it can start giving you the matching features. In this case, the code is calling getFeatures() once for every feature ID in the list. So if the list had 100 features, that means QGIS is having to create a connection to the data source, set up and prepare a query to match a single feature, wait for the provider to process that, and then finally parse the single feature result. That’s a lot of wasted processing!

If the code is rewritten to take the call to getFeatures() outside of the loop, then the result is:

request = QgsFeatureRequest().setFilterFids(some_list_of_feature_ids)
for feature in vector_layer.getFeatures(request):
    # do something with the feature

Now there’s just a single call to getFeatures() here. QGIS optimises this request by using a single connection to the data source, preparing the query just once, and fetching the results in appropriately sized batches. The difference is huge, especially if you’re dealing with a large number of features.

Trap #2: Use QgsFeatureRequest filters appropriately

Here’s another common mistake I see in PyQGIS code. I often see this one when an author is trying to do something with all the selected features in a layer:

for feature in vector_layer.getFeatures():
    if not feature.id() in vector_layer.selectedFeaturesIds():
        continue

    # do something with the feature

What’s happening here is that the code is iterating over all the features in the layer, and then skipping over any which aren’t in the list of selected features. See the problem here? This code iterates over EVERY feature in the layer. If you’re layer has 10 million features, we are fetching every one of these from the data source, going through all the work of parsing it into a QGIS feature, and then promptly discarding it if it’s not in our list of selected features. It’s very inefficient, especially if fetching features is slow (such as when connecting to a remote database source).

Instead, this code should use the setFilterFids() method for QgsFeatureRequest:

request = QgsFeatureRequest().setFilterFids(vector_layer.selectedFeaturesIds())
for feature in vector_layer.getFeatures(request):
    # do something with the feature

Now, QGIS will only fetch features from the provider with matching feature IDs from the list. Instead of fetching and processing every feature in the layer, only the actual selected features will be fetched. It’s not uncommon to see operations which previously took many minutes (or hours!) drop down to a few seconds after applying this fix.

Another variant of this trap uses expressions to test the returned features:

filter_expression = QgsExpression('my_field &gt; 20')
for feature in vector_layer.getFeatures():
    if not filter_expression.evaluate(feature):
        continue

    # do something with the feature

Again, this code is fetching every single feature from the layer and then discarding it if it doesn’t match the “my_field > 20” filter expression. By rewriting this to:

request = QgsFeatureRequest().setFilterExpression('my_field &gt; 20')
for feature in vector_layer.getFeatures(request):
    # do something with the feature

we hand over the bulk of the filtering to the data source itself. Recent QGIS versions intelligently translate the filter into a format which can be applied directly at the provider, meaning that any relevant indexes and other optimisations can be applied by the provider itself. In this case the rewritten code means that ONLY the features matching the ‘my_field > 20’ criteria are fetched from the provider – there’s no time wasted messing around with features we don’t need.

 

Trap #3: Only request values you need

The last trap I often see is that more values are requested from the layer then are actually required. Let’s take the code:

my_sum = 0
for feature in vector_layer.getFeatures(request):
    my_sum += feature['value']

In this case there’s no way we can optimise the filters applied, since we need to process every feature in the layer. But – this code is still inefficient. By default QGIS will fetch all the details for a feature from the provider. This includes all attribute values and the feature’s geometry. That’s a lot of processing – QGIS needs to transform the values from their original format into a format usable by QGIS, and the feature’s geometry needs to be parsed from it’s original type and rebuilt as a QgsGeometry object. In our sample code above we aren’t doing anything with the geometry, and we are only using a single attribute from the layer. By calling setFlags( QgsFeatureRequest.NoGeometry ) and setSubsetOfAttributes() we can tell QGIS that we don’t need the geometry, and we only require a single attribute’s value:

my_sum = 0
request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes(['value'], vector_layer.fields() )
for feature in vector_layer.getFeatures(request):
    my_sum += feature['value']

None of the unnecessary geometry parsing will occur, and only the ‘value’ attribute will be fetched and populated in the features. This cuts down both on the processing required AND the amount of data transfer between the layer’s provider and QGIS. It’s a significant improvement if you’re dealing with larger layers.

Conclusion

Optimising your feature requests is one of the easiest ways to speed up your PyQGIS script! It’s worth spending some time looking over all your uses of getFeatures() to see whether you can cut down on what you’re requesting – the results can often be mind blowing!

QGIS 3 is underway – what does it mean for your plugins and scripts?

With the imminent release of QGIS 2.16, the development attention has now shifted to the next scheduled release – QGIS 3.0! If you haven’t been following the discussion surrounding this I’m going to try and summarise what exactly 3.0 means and how it will impact any scripts or plugins you’ve developed for QGIS.

qgis_icon.svgQGIS 3.0 is the first major QGIS release since 2.0 was released way back in September 2013. Since that release so much has changed in QGIS… a quick glance over the release notes for 2.14 shows that even for this single point release there’s been hundreds of changes. Despite this, for all 2.x releases the PyQGIS API has remained stable, and a plugin or script which was developed for use in QGIS 2.0 will still work in QGIS 2.16.

Version 3.0 will introduce the first PyQGIS API break since 2013. An API break like this is required to move QGIS to newer libraries such as Qt 5 and Python 3, and allows the development team the flexibility to tackle long-standing issues and limitations which cannot be fixed using the 2.x API. Unfortunately, the side effect of this API break is that the scripts and plugins which you use in QGIS 2.x will no longer work when QGIS 3.0 is released!

Numerous API breaking changes have already started to flow into QGIS, and 2.16 isn’t even yet publicly available. The best way to track these changes is to keep an eye on the “API changes” documentation.  This document describes all the changes which are flowing in which affect PyQGIS code, and describe how best they should be addressed by plugin and script maintainers. Some changes are quite trivial and easy to update code for, others are more extreme (such as changes surrounding moving to PyQt5 and Python 3) and may require significant time to adapt for.

I’d encourage all plugin and script developers to keep watching the API break documentation, and subscribe to the developers list for additional information about required changes as they are introduced.

If you’re looking for assistance or to outsource adaptation of your plugins and scripts to QGIS 3.0 – the team at North Road are ideally placed to assist! Our team includes some of the most experienced QGIS developers who are directly involved with the development of QGIS 3.0, so you can be confident knowing that your code is in good hands. Just contact us to discuss your QGIS development requirements.

You can read more about QGIS 3.0 API changes in The road to QGIS 3.0 – part 1.

The road to QGIS 3.0 – part 1

qgis_icon.svgAs we discussed in QGIS 3 is under way, the QGIS project is working toward the next major version of the application and these developments have major impact on any custom scripts or plugins you’ve developed for QGIS.

We’re now just over a week into this work, and already there’s been tons of API breaking changes landing the code base. In this post we’ll explore some of these changes, what’s motivated them, and what they mean for your scripts.

The best source for keeping track of these breaking changes is to watch the API break documentation on GitHub. This file is updated whenever a change lands which potentially breaks plugins/scripts, and will eventually become a low-level guide to porting plugins to QGIS 3.0.

API clean-ups

So far, lots of the changes which have landed have related to cleaning up the existing API. These include:

Removal of deprecated API calls

The API has been frozen since QGIS 2.0 was released in 2013, and in the years since then many things have changed. As a result, different parts of the API were deprecated along the way as newer, better ways of doing things were introduced. The deprecated code was left intact so that QGIS 2.x plugins would still all function correctly. By removing these older, deprecated code paths it enables the QGIS developers to streamline the code, remove hacky workarounds, untested methods, and just generally “clean things up”. As an example, the older labelling system which pre-dates QGIS 2.0 (it had no collision detection, no curved labels, no fancy data defined properties or rule based labelling!) was still floating around just in case someone tried to open a QGIS 1.8 project. That’s all gone now, culling over 5000 lines of outdated, unmaintained code. Chances are this won’t affect your plugins in the slightest. Other removals, like the removal of QgsMapRenderer (the renderer used before multi-threaded rendering was introduced) likely have a much larger impact, as many scripts and plugins were still using QgsMapRenderer classes and calls. These all need to be migrated to the new QgsMapRendererJob and QgsMapSettings classes.

Renaming things for consistency

Consistent naming helps keep the API predictable and more user friendly. Lots of changes have landed so far to make the naming of classes and methods more consistent. These include things like:

  • Making sure names use consistent capitalization. Eg, there was previously methods named “writeXML” and “writeXml”. These have all been renamed to consistently use camel case, including for acronyms. (In case you’re wondering – this convention is used to follow the Qt library conventions).
  • Consistent use of terms. The API previously used a mix of “CRS” and “SRS” for similar purposes – it now consistently uses “CRS” for a coordinate reference system.
  • Removal of abbreviations. Lots of abbreviated words have been removed from the names, eg “destCrs” has become “destinationCrs”. The API wasn’t consistently using the same abbreviations (ie “dest”/”dst”/”destination”), so it was decided to remove all use of abbreviated words and replace them with the full word. This helps keep things predictable, and is also a bit friendlier for non-native English speakers.

The naming changes all need to be addressed to make existing scripts and plugins compatible with QGIS 3.0. It’s potentially quite a lot of work for plugin developers, but in the long term it will make the API easier to use.

Changes to return and argument types

There’s also been lots of changes relating to the types of objects returned by functions, or the types of objects used as function arguments. Most of these involve changing the c++ types from pointers to references, or from references to copies. These changes are being made to strengthen the API and avoid potential crashes. In most cases they don’t have any affect on PyQGIS code, with some exceptions:

  • Don’t pass Python “None” objects as QgsCoordinateReferenceSystems or as QgsCoordinateTransforms. In QGIS 3.0 you must pass invalid QgsCoordinateReferenceSystem objects (“QgsCoordinateReferenceSystem()”) or invalid QgsCoordinateTransform (“QgsCoordinateTransform()”) objects instead.

Transparent caching of CRS creation

The existing QgsCRSCache class has been removed. This class was used to cache the expensive results of initializing a QgsCoordinateReferenceSystem object, so that creating the same CRS could be done instantly and avoid slow databases lookups. In QGIS 3.0 this caching is now handled transparently, so there is no longer a need for the separate QgsCRSCache and it has been removed. If you were using QgsCRSCache in your PyQGIS code, it will need to be removed and replaced with the standard QgsCoordinateReferenceSystem constructors.

This change has the benefit that many existing plugins which were not explicitly using QgsCRSCache will now gain the benefits of the faster caching mechanism – potentially this could dramatically speed up existing plugin algorithms.

In summary

The QGIS developers have been busy fixing, improving and cleaning up the PyQGIS API. We recognise that these changes result in significant work for plugin and script developers, so we’re committed to providing quality documentation for how to adapt your code for these changes, and we will also investigate the use of automated tools to help ease your code transition to QGIS 3.0. We aren’t making changes lightly, but instead are carefully refining the API to make it more predictable, streamlined and stable.

If you’d like assistance with (or to outsource) the transition of your existing QGIS scripts and plugins to QGIS 3.0, just contact us at North Road to discuss. Every day we’re directly involved in the changes moving to QGIS 3.0, so we’re ideally placed to make this transition painless for you!

Open source IDF router for QGIS

This is a follow-up on my previous post introducing an Open source IDF parser for QGIS. Today’s post takes the code further and adds routing functionality for foot, bike, and car routes including oneway streets and turn restrictions.

You can find the script in my QGIS-resources repository on Github. It creates an IDFRouter object based on an IDF file which you can use to compute routes.

The following screenshot shows an example car route in Vienna which gets quite complex due to driving restrictions. The dark blue line is computed by my script on GIP data while the light blue line is the route from OpenRouteService.org (via the OSM route plugin) on OSM data. Minor route geometry differences are due to slight differences in the network link geometries.

Screenshot 2015-08-01 16.29.57


Open source IDF parser for QGIS

IDF is the data format used by Austrian authorities to publish the official open government street graph. It’s basically a text file describing network nodes, links, and permissions for different modes of transport.

Since, to my knowledge, there hasn’t been any open source IDF parser available so far, I’ve started to write my own using PyQGIS. You can find the script which is meant to be run in the QGIS Python console in my Github QGIS-resources repo.

I haven’t implemented all details yet but it successfully parses nodes and links from the two example IDF files that have been published so far as can be seen in the following screenshot which shows the Klagenfurt example data:

Screenshot 2015-07-23 16.23.25

If you are interested in advancing this project, just get in touch here or on Github.


A interactive command bar for QGIS

Something that has been on my mind for a long time is a interactive command interface for QGIS.  Something that you can easily open, run simple commands, and is interactive to ask for arguments when they are needed.

After using the command interface in Emacs for a little bit over the weekend – you can almost hear the Boos! from heavy Vim users :) – I thought this is something I must have in QGIS as well.  I’m sure it can’t be that hard to add.

So here it is.  A interactive command interface for QGIS.

commandbar

commandbar2

The command bar plugin (find it in the plugin installer) adds a simple interactive command bar to QGIS. Commands are defined as Python code and may take arguments.

Here is an example function:

@command.command("Name")
def load_project(name):
    """
    Load a project from the set project paths
    """
    _name = name
    name += ".qgs"
    for path in project_paths:
        for root, dirs, files in os.walk(path):
            if name in files:
                path = os.path.join(root, name)
                iface.addProject(path)
                return
    iface.addProject(_name)

All functions are interactive and if not all arguments are given when called it will prompt for each one.

Here is an example of calling the point-at function with no args. It will ask for the x and then the y

pointat

Here is calling point-at with all the args

pointatfunc

Functions can be called in the command bar like so:

my-function arg1 arg2 arg2

The command bar will split the line based on space and the first argument is always the function name, the rest are arguments passed to the function. You will also note that it will convert _ to - which is easier to type and looks nicer.

The command bar also has auto complete for defined functions – and tooltips once I get that to work correctly.

You can use CTRL + ; (CTRL + Semicolon), or CTRL + ,, to open and close the command bar.

What is a command interface without auto complete

autocomplete

Use Enter to select the item in the list.

How about a function to hide all the dock panels. Sure why not.

@command.command()
def hide_docks():
    docks = iface.mainWindow().findChildren(QDockWidget)
    for dock in docks:
        dock.setVisible(False)

alias command

You can also alias a function by calling the alias function in the command bar.

The alias command format is alias {name} {function} {args}

Here is an example of predefining the x for point-at as mypoint

-> alias mypoint point-at 100

point-at is a built in function that creates a point at x y however we can alias it so that it will be pre-called with the x argument set. Now when we call mypoint we only have to pass the y each time.

-> mypoint
(point-at) What is the Y?: 200

You can even alias the alias command – because why the heck not :)

-> alias a alias
a mypoint 100

a is now the shortcut hand for alias

WHY U NO USE PYTHON CONSOLE

The Python console is fine and dandy but we are not going for a full programming language here, that isn’t the point. The point is easy to use commands.

You could have a function called point_at in Python that would be

point_at(123,1331)

Handling incomplete functions is a lot harder because of the Python parser. In the end it’s easier and better IMO to just make a simple DSL for this and get all the power of a DSL then try and fit into Python.

It should also be noted that the commands defined in the plugin can still be called like normal Python functions because there is no magic there. The command bar is just a DSL wrapper around them.

Notes

This is still a bit of an experiment for me so things might change or things might not work as full expected just yet.

Check out the projects readme for more info on things that need to be done, open to suggestions and pull requests.

Also see the docs page for more in depth information


Filed under: Open Source, python, qgis Tagged: plugin, pyqgis, qgis

PyQGIS Resources

Here is a short list of resources available when writing Python code in QGIS. If you know of others, please leave a comment.

Blogs/Websites

In alphabetical order:

Documentation

Example Code

  • Existing plugins can be a great learning tool
  • Code Snippets in the PyQGIS Cookbook

Plugins/Tools

  • Script Runner: Run scripts to automate QGIS tasks
  • Plugin Builder: Create a starter plugin that you can customize to complete your own plugin
  • Plugin Reloader: Allows you to reload a plugin from within QGIS
  • pb_tool: Tool to compile and deploy your plugins

Books

Plugin Builder 2.8.1

This minor update to the Plugin Builder allows you to choose where your plugin menu will be located.

Previously your menu was placed under the Plugins menu. At version 2.8.1 you can choose from the following main menu locations:

  • Plugins
  • Database
  • Raster
  • Vector
  • Web

Plugins is the default choice when you open Plugin Builder. The value you choose is also written to the category field in your metadata.txt file. When you view your plugin in the Plugin Manager, the value of category is displayed, aiding folks in finding the menu location.

You can install Plugin Builder 2.8.1 from the Plugins -> Manage and Install Plugins... menu. Version 2.8.1 works on QGIS versions 2.0 and up.

Plugin Builder 2.8

Plugin Builder 2.8 is now available. This is a minor update that adds:

  • Suggestion for setting up an issue tracker and creating a code repository
  • Suggestion for a home page
  • Tag selection from a list of current tags
  • Documentation update, including information about using pb_tool to compile, deploy, and package your plugin
  • New URLs for Plugin Builder's home page and bug tracking

Optional is now Recommended

In previous versions the following items were "Optional" when creating a new plugin:

  • Bug tracker
  • Home page
  • Repository
  • Tags

We've changed those from "Optional" to "Recommended" because they are important for the success and longevity of your plugin(s). Setting up a code repository on GitHub automatically gives you issue tracking and the ability for others to collaborate with fixes and enhancements through pull requests.

Using GitHub also gives you the ability to setup a home page right from your repository using GitHub pages.

Adding one or more tags to your plugin helps people find them easier when browsing the QGIS Plugins website.

Getting It

You can install Plugin Builder 2.8 from the Plugins -> Manage and Install Plugins... menu. Version 2.8 works on QGIS versions 2.0 and up.

The PyQGIS Programmer's Guide

The PyQGIS Programmer's Guide is now available in both paperback and PDF. A sample chapter is also available for download.

The book is fully compatible with the QGIS 2.x series of releases.

Faking a Data Provider with Python

QGIS data providers are written in C++, however it is possible to simulate a data provider in Python using a memory layer and some code to interface with your data.

Why would you want to do this? Typically you should use the QGIS data providers, but here are some reasons why you may want to give it a go:

  • There is no QGIS data provider
  • The generic access available through OGR doesn't provide all the features you need
  • You have no desire to write a provider in C++
  • No one will write a C++ provider for you, for any amount of money

If you go this route you are essentially creating a bridge that connects QGIS and your data store, be it flat file, database, or some other binary format. If Python can "talk" to your data store, you can write a pseudo-provider.

To illustrate the concept, we'll create a provider for CSV files that allows you to create a layer and have full editing capabilities using QGIS and the Python csv module.

The provider will:

  • Create a memory layer from a CSV file
  • Create fields in the layer based on integer, float, or string values in the CSV
  • Write changes back to the CSV file
  • Require the CSV file to have an X and Y field
  • Support only Point geometries

We'll use the cities.shp file that comes with the PyQGIS Programmer's Guide to create a CSV using ogr2ogr:

ogr2ogr -lco "GEOMETRY=AS_XY" -f CSV cities.csv ~/Downloads/pyqgis_data/cities.shp

This gives us a CSV file with point geometry fields.

If you don't want to roll your own, you can download the cities.csv file here.

Here's the header (first row) and a couple of rows from the file:

X,Y,NAME,COUNTRY,POPULATION,CAPITAL
33.086040496826172,68.963546752929688,Murmansk,Russia,468000,N
40.646160125732422,64.520668029785156,Arkhangelsk,Russia,416000,N
30.453327178955078,59.951889038085938,Saint Petersburg,Russia,5825000,N

Signals and Slots

We'll look at the code in a moment, but here is the general flow and connection of signals from the layer to slots (methods) in our provider:

These are all the connections we need (signal -> slot) to implement editing of both geometries and attributes for the layer.

The Plugin Code

The plugin is pretty simple; when the tool is clicked, it opens a dialog allowing you to select a CSV file, then uses the CsvLayer class to read the file, create the memory layer, make connections (signals -> slots), and add it to the QGIS canvas.

The run Method

Here's the run method from the plugin (csv_provider.py):

1
2
3
4
5
6
7
8
9
10
def run(self):
    """Run method that performs all the real work"""
    # show the dialog
    self.dlg.show()
    # Run the dialog event loop
    result = self.dlg.exec_()
    # See if OK was pressed
    if result:
        csv_path = self.dlg.lineEdit.text()
        self.csvlayer = CsvLayer(csv_path)

The dialog (csv_provider_dialog.py) contains an intimidating warning and allows you to select a CSV file to load (lines 4-6).

Line 9 gets the path of the selected file from the dialog and line 10 creates the layer using the selected file. From that point on, you interact with the layer using the regular QGIS editing tools. We need to keep a reference to the layer (self.csvlayer), otherwise all our connections get garbage collected and we lose the "link" to the CSV file.


The CsvLayer Class

The CsvLayer class manages the creation, loading, and editing of the CSV file. First let's look at the methods in csv_layer.py that create the layer from our CSV file.

Creating the Layer from the CSV

The __init__ method examines the header and a sample row to determine the names and field types to be created in the layer. To read the CSV file, we use the csv module.

Here's the __init__ method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(self, csv_path):
    """ Initialize the layer by reading the CSV file, creating a memory
    layer, and adding records to it """
    # Save the path to the file so we can update it in response to edits
    self.csv_path = csv_path
    self.csv_file = open(csv_path, 'rb')
    self.reader = csv.reader(self.csv_file)
    self.header = self.reader.next()
    logger(str(self.header))
    # Get sample
    sample = self.reader.next()
    self.field_sample = dict(zip(self.header, sample))
    logger("sample %s" % str(self.field_sample))
    field_name_types = {}
    # create dict of fieldname:type
    for key in self.field_sample.keys():
        if self.field_sample[key].isdigit():
            field_type = 'integer'
        else:
            try:
                float(self.field_sample[key])
                field_type = 'real'
            except ValueError:
                field_type = 'string'
        field_name_types[key] = field_type
    logger(str(field_name_types))
    # Build up the URI needed to create memory layer
    self.uri = self.uri = "Point?crs=epsg:4326"
    for fld in self.header:
        self.uri += '&field={}:{}'.format(fld, field_name_types[fld])

    logger(self.uri)
    # Create the layer
    self.lyr = QgsVectorLayer(self.uri, 'cities.csv', 'memory')
    self.add_records()
    # done with the csv file
    self.csv_file.close()

    # Make connections
    self.lyr.editingStarted.connect(self.editing_started)
    self.lyr.editingStopped.connect(self.editing_stopped)
    self.lyr.committedAttributeValuesChanges.connect(self.attributes_changed)
    self.lyr.committedFeaturesAdded.connect(self.features_added)
    self.lyr.committedFeaturesRemoved.connect(self.features_removed)
    self.lyr.geometryChanged.connect(self.geometry_changed)

    # Add the layer the map
    QgsMapLayerRegistry.instance().addMapLayer(self.lyr)

Once the CSV file is opened, the header is read in line 8, and a sample of the first row is read in line 11.

Line 12 creates a dict that maps the field names from the header to the corresponding values in the sample row.

Line 16-25 look at each sample value and determine if its type: integer, real, or string.

Lines 28-30 create the URI needed to create the memory layer, which is done in line 34.

The add_records method is called to read the CSV and add the features in line 35.

Lastly we make the connections needed to support editing of the attribute table and the CSV file in response to the actions in our Signal/Slot diagram (lines 40-45).

Here is the add_records method that reads the CSV and creates a corresponding feature in the newly created memory layer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def add_records(self):
    """ Add records to the memory layer by reading the CSV file """
    # Return to beginning of csv file
    self.csv_file.seek(0)
    # Skip the header
    self.reader.next()
    self.lyr.startEditing()

    for row in self.reader:
        flds = dict(zip(self.header, row))
        # logger("This row: %s" % flds)
        feature = QgsFeature()
        geometry = QgsGeometry.fromPoint(
            QgsPoint(float(flds['X']), float(flds['Y'])))

        feature.setGeometry(geometry)
        feature.setAttributes(row)
        self.lyr.addFeature(feature, True)
    self.lyr.commitChanges()

Each row in the CSV file is read and both the attributes and geometry for the feature are created and added to the layer.

Managing Changes

With the layer loaded and the connections made, we can edit the CSV using the regular QGIS editing tools. Here is the code for the connections that make this happen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def editing_started(self):
    """ Connect to the edit buffer so we can capture geometry and attribute
    changes """
    self.lyr.editBuffer().committedAttributeValuesChanges.connect(
        self.attributes_changed)

def editing_stopped(self):
    """ Update the CSV file if changes were committed """
    if self.dirty:
        logger("Updating the CSV")
        features = self.lyr.getFeatures()
        tempfile = NamedTemporaryFile(mode='w', delete=False)
        writer = csv.writer(tempfile, delimiter=',')
        # write the header
        writer.writerow(self.header)
        for feature in features:
            row = []
            for fld in self.header:
                row.append(feature[feature.fieldNameIndex(fld)])
            writer.writerow(row)

        tempfile.close()
        shutil.move(tempfile.name, self.csv_path)

        self.dirty = False

def attributes_changed(self, layer, changes):
    """ Attribute values changed; set the dirty flag """
    if not self.doing_attr_update:
        logger("attributes changed")
        self.dirty = True

def features_added(self, layer, features):
    """ Features added; update the X and Y attributes for each and set the
    dirty flag
    """
    logger("features added")
    for feature in features:
        self.geometry_changed(feature.id(), feature.geometry())
    self.dirty = True

def features_removed(self, layer, feature_ids):
    """ Features removed; set the dirty flag """
    logger("features removed")
    self.dirty = True

def geometry_changed(self, fid, geom):
    """ Geometry for a feature changed; update the X and Y attributes for
    each """
    feature = self.lyr.getFeatures(QgsFeatureRequest(fid)).next()
    pt = geom.asPoint()
    logger("Updating feature {} ({}) X and Y attributes to: {}".format(
        fid, feature['NAME'], pt.toString()))
    self.lyr.changeAttributeValue(fid, feature.fieldNameIndex('X'),
                                  pt.x())
    self.lyr.changeAttributeValue(fid, feature.fieldNameIndex('Y'),
                                  pt.y())

The dirty flag is used to indicate that the CSV file needs to be updated. Since the format doesn't support random access to individual rows, we rewrite the entire file each time an update is needed. This happens in the editing_stopped method.

When attributes are changed and/or removed, the only action taken is to set the dirty flag to True.

When there is a geometry change for a feature, it is updated in the attribute table immediately to keep the X and Y values in sync with the feature's coordinates. This happens in the geometry_changed method.

You can view the full code for both the CsvLayer class and the plugin on GitHub or by installing the plugin. To install the plugin, you'll need to make sure you have the Show also experimental plugins checkbox ticked in the Plugin Manager settings. To help you find it, the plugin is listed as CSV Provider in the Plugin Manager.

Looking Further

A few things about this implementation:

  • It is an example, not a robust implementation
  • It lacks proper error handling
  • There is no help file
  • It could be extended to support other geometry types in CSV
  • In its current form, it may not work for other CSV files, depending on how they are formatted (take a look at the Add Delimited Text Layer dialog in QGIS to see what I mean)
  • If you want to enhance this for fun or profit---well for fun, fork the repository and give me some pull requests

A couple of final points:

  • Going this route can be a lot more work than using an existing data provider
  • This method can be (and has been) used successfully to interface with a data store for which there is no data provider

A Quick Guide to Getting Started with PyQGIS on Windows

Getting started with Python and QGIS can be a bit overwhelming. In this post we give you a quick start to get you up and running and maybe make your PyQGIS life a little easier.

There are likely many ways to setup a working PyQGIS development environment---this one works pretty well.

Contents

Requirements

  • OSGeo4W Advanced Install of QGIS
  • pip (for installing/managing Python packages)
  • pb_tool (cross-platform tool for compiling/deploying/distributing QGIS plugin)
  • A customized startup script to set the environment (pyqgis.cmd)
  • IDE (optional)
  • Vim (just kidding)

We'll start with the installs.

Installing

Almost everything we need can be installed using the OSGeo4W installer available on the QGIS website.

OSGeo4W

From the QGIS website, download the appropriate network installer (32 or 64 bit)

  • Run the installer and choose the Advanced Install option
  • Install from Internet
  • Choose a directory for the install---I prefer a path without spaces such as C:\OSGeo4W
  • Accept default for local package directory and Start menu name
  • Tweak network connection option if needed on the Select Your Internet Connection screen
  • Accept default download site location
  • From the Select packages screen, select the following for installation:
    • Desktop -> qgis: QGIS Desktop
    • Libs -> qt4-devel (needed for lrelease/translations)
    • Libs -> setuptools (needed for installing pip)

When you click Next a bunch of additional packages will be suggested---just accept them and continue the install.

Once complete you will have a functioning QGIS install along with the other parts we need. If you want to work with the nightly build of QGIS, choose Desktop -> qgis-dev instead.

If you've already installed QGIS using the OSGeo4W installer, just install the qt4-devel and setutools packages. If you installed QGIS using the standalone installer, the easiest option is to remove it and install from OSGeo4W. You can run both the standalone and OSGeo4W versions on the same machine, but you need to be extra careful not to mix up the environment.

Setting the Environment

To continue with the setup, we need to set the environment by creating a .cmd script. The following is adapted from several sources, and trimmed down to the minimum. Copy and paste it into a file named pyqgis.cmd and save it to a convenient location (like your HOME directory).

@echo off
SET OSGEO4W_ROOT=C:\OSGeo4W
call "%OSGEO4W_ROOT%"\bin\o4w_env.bat
call "%OSGEO4W_ROOT%"\apps\grass\grass-6.4.3\etc\env.bat
@echo off
path %PATH%;%OSGEO4W_ROOT%\apps\qgis\bin
path %PATH%;%OSGEO4W_ROOT%\apps\grass\grass-6.4.3\lib

set PYTHONPATH=%PYTHONPATH%;%OSGEO4W_ROOT%\apps\qgis\python;
set PYTHONPATH=%PYTHONPATH%;%OSGEO4W_ROOT%\apps\Python27\Lib\site-packages
set QGIS_PREFIX_PATH=%OSGEO4W_ROOT%\apps\qgis
set PATH=C:\Program Files (x86)\Git\cmd;C:\Program Files (x86)\Vim\vim74;%PATH%
cd %HOMEPATH%\development
cmd.exe

You should customize the set PATH statement to add any paths you want available when working from the command line. I added paths to my git and vim installs.

The cd %HOMEPATH%\development statement starts the shell in my normal working directory---customize or remove as you see fit.

The last line starts a cmd shell with the settings specified above it. We'll see an example of starting an IDE in a bit.

You can test to make sure all is well by double-clicking on our pyqgis.cmd script, then starting Python and attempting to import one of the QGIS modules:

C:\Users\gsherman\development>python
Python 2.7.5 (default, May 15 2013, 22:44:16) [MSC v.1500 64 bit (AMD64)] on win
32
Type "help", "copyright", "credits" or "license" for more information.
>>> import qgis.core
>>>

If you don't get any complaints on import, things are looking good.

Python Packages

We need a couple of Python packages as well.

pip

There are several ways to install pip, but since we installed setuptools we can use easy_install:

easy_install pip

Make sure to issue this command from your customized shell (double-click on pyqgis.cmd to start it).

pb_tool

With pip installed we can use it to install pb_tool:

pip install pb_tool

More information on using pb_tool is available on the project website.

Working on the Command Line

Just double-click on your pyqgis.cmd script from the Explorer or a desktop shortcut to start a cmd shell. From here you can use Python interactively and also use pb_tool to compile and deploy your plugin for testing.

IDE Example

With slight modification, we can start our IDE with the proper settings to recognize the QGIS libraries:

@echo off
SET OSGEO4W_ROOT=C:\OSGeo4W
call "%OSGEO4W_ROOT%"\bin\o4w_env.bat
call "%OSGEO4W_ROOT%"\apps\grass\grass-6.4.3\etc\env.bat
@echo off
path %PATH%;%OSGEO4W_ROOT%\apps\qgis\bin
path %PATH%;%OSGEO4W_ROOT%\apps\grass\grass-6.4.3\lib

set PYTHONPATH=%PYTHONPATH%;%OSGEO4W_ROOT%\apps\qgis\python;
set PYTHONPATH=%PYTHONPATH%;%OSGEO4W_ROOT%\apps\Python27\Lib\site-packages
set QGIS_PREFIX_PATH=%OSGEO4W_ROOT%\apps\qgis
set PATH=C:\Program Files (x86)\Git\cmd;C:\Program Files (x86)\Vim\vim74;%PATH%
cd %HOMEPATH%\development
start "PyCharm aware of Quantum GIS" /B "C:\Program Files (x86)\JetBrains\PyCharm 3.4.1\bin\pycharm.exe" %*

We only changed the last line, adding the start statement with the path to the IDE (PyCharm). If you save this to something like pycharm.cmd, you can double-click on it to start PyCharm. The same method works for other IDEs, such as PyDev.

Within your IDE settings, point it to use the Python interpreter included with OSGeo4W---typically at: %OSGEO4W_ROOT%\bin\python.exe. This will make it pick up all the QGIS goodies needed for development, completion, and debugging. In my case OSGEO4W_ROOT is C:\OSGeo4W, so in the IDE, the path to the correct Python interpreter would be: C:\OSGeo4W\bin\python.exe.

Make sure you adjust the paths in your .cmd scripts to match your system and software locations.

Workflow

Here is an example of a workflow you can use once you're setup for development.

Creating a New Plugin

  1. Use the Plugin Builder plugin to create a starting point [1]
  2. Start your pyqgis.cmd shell
  3. Use pb_tool to compile and deploy the plugin (pb_tool deploy will do it all in one pass)
  4. Activate it in QGIS and test it out
  5. Add code, deploy, test, repeat

Working with Existing Plugin Code

The steps are basically the same was creating a new plugin, except we start by using pb_tool to create a new config file:

  1. Start your pyqgis.cmd shell
  2. Change to the directory containing your plugin code
  3. Use pb_tool create to create a config file
  4. Edit pb_tool.cfg to adjust/add things create may have missed
  5. Start at step 3 in Creating a New Plugin and press on

Troubleshooting

Assuming you have things properly installed, trouble usually stems from an incorrect environment.

  • Make sure QGIS runs and the Python console is available and working
  • Check all the paths in your pygis.cmd or your custom IDE cmd script
  • Make sure your IDE is using the Python interpreter that comes with OSGeo4W


[1] Plugin Builder 2.6 will support generation of a pb_tool config file

  • Page 1 of 2 ( 29 posts )
  • >>
  • pyqgis

Back to Top

Sponsors