Table of Contents for
Python Geospatial Development - Third Edition

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition Python Geospatial Development - Third Edition by Erik Westra Published by Packt Publishing, 2016
  1. Cover
  2. Table of Contents
  3. Python Geospatial Development Third Edition
  4. Python Geospatial Development Third Edition
  5. Credits
  6. About the Author
  7. About the Reviewer
  8. www.PacktPub.com
  9. Preface
  10. What you need for this book
  11. Who this book is for
  12. Conventions
  13. Reader feedback
  14. Customer support
  15. 1. Geospatial Development Using Python
  16. Geospatial development
  17. Applications of geospatial development
  18. Recent developments
  19. Summary
  20. 2. GIS
  21. GIS data formats
  22. Working with GIS data manually
  23. Summary
  24. 3. Python Libraries for Geospatial Development
  25. Dealing with projections
  26. Analyzing and manipulating Geospatial data
  27. Visualizing geospatial data
  28. Summary
  29. 4. Sources of Geospatial Data
  30. Sources of geospatial data in raster format
  31. Sources of other types of geospatial data
  32. Choosing your geospatial data source
  33. Summary
  34. 5. Working with Geospatial Data in Python
  35. Working with geospatial data
  36. Changing datums and projections
  37. Performing geospatial calculations
  38. Converting and standardizing units of geometry and distance
  39. Exercises
  40. Summary
  41. 6. Spatial Databases
  42. Spatial indexes
  43. Introducing PostGIS
  44. Setting up a database
  45. Using PostGIS
  46. Recommended best practices
  47. Summary
  48. 7. Using Python and Mapnik to Generate Maps
  49. Creating an example map
  50. Mapnik concepts
  51. Summary
  52. 8. Working with Spatial Data
  53. Designing and building the database
  54. Downloading and importing the data
  55. Implementing the DISTAL application
  56. Using DISTAL
  57. Summary
  58. 9. Improving the DISTAL Application
  59. Dealing with the scale problem
  60. Performance
  61. Summary
  62. 10. Tools for Web-based Geospatial Development
  63. A closer look at three specific tools and techniques
  64. Summary
  65. 11. Putting It All Together – a Complete Mapping System
  66. Designing the ShapeEditor
  67. Prerequisites
  68. Setting up the database
  69. Setting up the ShapeEditor project
  70. Defining the ShapeEditor's applications
  71. Creating the shared application
  72. Defining the data models
  73. Playing with the admin system
  74. Summary
  75. 12. ShapeEditor – Importing and Exporting Shapefiles
  76. Importing shapefiles
  77. Exporting shapefiles
  78. Summary
  79. 13. ShapeEditor – Selecting and Editing Features
  80. Editing features
  81. Adding features
  82. Deleting features
  83. Deleting shapefiles
  84. Using the ShapeEditor
  85. Further improvements and enhancements
  86. Summary
  87. Index

Editing features

Now that we know which feature the user wants to edit, our next task is to implement the "Edit Feature" page itself. To do this, we are going to have to create a custom form with a single input field, named geometry, that uses a suitable map-editing widget for editing the feature's geometry.

The process of building this form is a bit involved, thanks to the fact that we have to create a new django.contrib.gis.forms.Form subclass on the fly to handle the different types of geometries that can be edited. Let's put this complexity into a new function within the shared.utils module, which we'll call get_map_frm().

Edit the utils.py module and type in the following code:

def get_map_form(shapefile):
    geometry_field = calc_geometry_field(shapefile.geom_type)

    if geometry_field == "geom_multipoint":
        field = forms.MultiPointField
    elif geometry_field == "geom_multilinestring":
        field = forms.MultiLineStringField
    elif geometry_field == "geom_multipolygon":
        field = forms.MultiPolygonField
    elif geometry_field == "geom_geometrycollection":
        field = forms.GeometryCollectionField
    else:
        raise RuntimeError("Unsupported field: " + geometry_field)

       widget = forms.OpenLayersWidget()

    class MapForm(forms.Form):
        geometry = field(widget=widget)

    return MapForm

You'll also need to add the following import statement to the top of the file:

from django.contrib.gis import forms

The get_map_form() function selects the type of field to edit based on the shapefile's geom_type attribute. For example, if the shapefile contains Point or MultiPoint geometries, we select the MultiPointField class for editing the shapefile's data. Once the field type has been selected, we dynamically create a new django.contrib.gis.forms.Form subclass that uses an instance of that form, with an OpenLayersWidget for editing the geometry's data.

Notice that the get_map_form() function returns the MapForm class rather than an instance of that class; we'll use the returned class to create the appropriate MapForm instances as we need them.

With this function behind us, we can now implement the rest of the "Edit Feature" view. Let's start by setting up the view's URL: open the urls.py module and add the following to the list of URL patterns:

  url(r'^edit_feature/(?P<shapefile_id>\d+)/(?P<feature_id>\d+)$',
      shapeEditor.shapefiles.views.edit_feature),

We're now ready to implement the view function itself. Edit the shapefiles.views module and start defining the edit_feature() function:

def edit_feature(request, shapefile_id, feature_id):
  try:
    shapefile = Shapefile.objects.get(id=shapefile_id)
  except ShapeFile.DoesNotExist:
    return HttpResponseNotFound()

  try:
    feature = Feature.objects.get(id=feature_id)
  except Feature.DoesNotExist:
    return HttpResponseNotFound()

So far, this is quite straightforward: we load the Shapefile object for the current shapefile and the Feature object for the feature we are editing. We next want to load into memory a list of that feature's attributes so that they can be displayed to the user:

  attributes = []
  for attr_value in feature.attributevalue_set.all():
    attributes.append([attr_value.attribute.name,
                       attr_value.value])
  attributes.sort()

This is where things get interesting. We need to create a Django Form object (actually, an instance of the MapForm class created dynamically by the get_map_form() function we wrote earlier), and use this form instance to display the feature to be edited. When the form is submitted, we'll extract the updated geometry and save it back into the Feature object again before redirecting the user back to the "Edit Shapefile"page to select another feature.

As we saw when we created the "Import Shapefile" form, the basic Django idiom for processing a form is as follows:

if request.method == "GET":
    form = MyForm()
    return render(request, "template.html",
                  {'form' : form})
elif request.method == "POST":
    form = MyForm(request.POST)
    if form.is_valid():
        # Extract and save the form's contents here...
        return HttpResponseRedirect("/somewhere/else")
    return render(request, "template.html",
                  {'form' : form})

When the form is to be displayed for the first time, request.method will have a value of GET. In this case, we create a new form object and display the form as part of an HTML template. When the form is submitted by the user, request.method will have the value POST. In this case, a new form object is created that is bound to the submitted POST arguments. The form's contents are then checked, and if they are valid, they are saved and the user is redirected to some other page. If the form is not valid, it will be displayed again along with a suitable error message.

Let's see how this idiom is used by the "Edit Feature" view. Add the following to the end of your new view function:

  geometry_field = \
       utils.calc_geometry_field(shapefile.geom_type)
  form_class = utils.get_map_form(shapefile)

  if request.method == "GET":
    wkt = getattr(feature, geometry_field)
    form = form_class({'geometry' : wkt})

    return render(request, "edit_feature.html",
                  {'shapefile'  : shapefile,
                   'form'       : form,
                   'attributes' : attributes})
  elif request.method == "POST":
    form = form_class(request.POST)
    try:
      if form.is_valid():
        wkt = form.cleaned_data['geometry']
        setattr(feature, geometry_field, wkt)
        feature.save()
        return HttpResponseRedirect("/edit/" +
                                    shapefile_id)
    except ValueError:
      pass

    return render(request, "edit_feature.html",
                  {'shapefile'  : shapefile,
                   'form'       : form,
                   'attributes' : attributes})

As you can see, we call utils.get_map_form() to create a new django.forms.Form subclass, which will be used to edit the feature's geometry. We also call utils.calc_geometry_field() to see which field in the Feature object should be edited.

The rest of this function pretty much follows the Django idiom for form processing. The only interesting thing to note is that we get and set the geometry field (using the getattr() and setattr() functions, respectively) in WKT format. GeoDjango treats geometry fields as if they were character fields that hold the geometry in WKT format. The GeoDjango JavaScript code then takes that WKT data (which is stored in a hidden form field named geometry) and passes it to OpenLayers for display as a vector geometry. OpenLayers allows the user to edit that vector geometry, and the updated geometry is stored back into the hidden geometry field as WKT data. We then extract that updated geometry's WKT text and store it back into the Feature object.

That's all you need to know about the edit_feature() view function. Let's now create the template used by this view. Create a new file named edit_feature.html within the shapefiles application's templates directory, and enter the following text into it:

<html>
  <head>
    <title>ShapeEditor</title>
    {{ form.media }}
  </head>
  <body>
    <h1>Edit Feature</h1>
    <form method="POST" action="">
      <table>
        {{ form.as_table }}
        <tr>
          <td></td>
          <td align="right">
            <table>
{% for attr in attributes %}
              <tr>
                <td>{{ attr.0 }}</td>
                <td>{{ attr.1 }}</td>
              </tr>
{% endfor %}
            </table>
          </td>
        </tr>
        <tr>
          <td></td>
          <td align="center">
            <input type="submit" value="Save"/>
            &nbsp;
            <button type="button" onClick='window.location="/edit/{{ shapefile.id }}";'>
              Cancel
            </button>
          </td>
        </tr>
      </table>
    </form>
  </body>
</html>

This template uses an HTML table to display the form and calls the form.as_table template function to render the form as HTML table rows. We then display a list of the feature's attributes as additional rows within this table and, finally, add Save and Cancel buttons to the bottom of the form.

With all this code written, we are finally able to edit features within the ShapeEditor:

Editing features

Within this editor, you can make use of a number of GeoDjango's built-in features to edit a geometry:

  • You can click on the Edit Geometry tool (Editing features) to select a feature for editing.
  • You can click on the Add Geometry tool (Editing features) to start drawing a new geometry.
  • When a geometry is selected, you can click on a dark circle and drag it to move the endpoints of a line segment.
  • When a geometry is selected, you can click on a light circle to split an existing line segment in two, creating a new point which can then be dragged.
  • If you hold the mouse down over a dark circle, you can press the Delete key (or type D) to delete that point. Note that this only works if the geometry has more than three points.
  • You can click on the Delete all Features hyperlink to delete the current feature's geometries. We'll look at this hyperlink in more detail shortly.

Once you have finished editing the feature, you can click on the Save button to save the edited features or the Cancel button to abandon the changes.

While this is all working well, there is one rather annoying quirk: GeoDjango lets the user remove the geometries from a map using a hyperlink named Delete all Features. Since we're currently editing a single feature within the shapefile, this hyperlink is rather confusingly named: what it actually does is delete the geometries for this feature, not the feature itself. Let's change the text of this hyperlink to something more meaningful.

Go to the copy of Django that you downloaded, and navigate to the contrib/gis/templates/gis directory. In this directory is a file named openlayers.html. Take a copy of this file, and move it into your shapefiles application's templates directory, renaming it to openlayers-custom.html.

Open your copy of this file, and look near the bottom for the text Delete all Features. Change this to Clear Feature's Geometry, and save your changes.

So far, so good. Now, we need to tell the GeoDjango editing widget to use our custom version of the openlayers.html file. To do this, edit your utils.py module and find your definition of the get_map_form() function. Add the following code immediately after the line that reads widget = forms.OpenLayersWidget():

    widget.template_name = "openlayers-custom.html"

If you then try editing a feature, you'll see that your customized version of the openlayers.html file is being used:

Editing features

By replacing the template, passing parameters to the widget when it is created, and creating your own custom subclass of the BaseGeometryWidget class, you can make various changes to the appearance and functionality of the geometry-editing widget. If you want to see what is possible, take a look at the modules in the django.contrib.gis directory.