Search code examples
djangoopenlayersgisgeodjango

Send GeoDjango queryset to template and consume with OpenLayers.js


I have been learning to use GeoDjango recently and I have been working through a few tutorials to try to understand how everything is put together. I am also new to GIS, but relatively comfortable with Django.

I have specifically been following along this tutorial, which is great if not a bit outdated: https://code.google.com/p/geodjango-basic-apps/wiki/FOSS4GWorkshop

I have made it to section 8, updating the django stuff where I can and also trying to get around the deprecated OpenLayers stuff, but I've hit a wall with something.

The tutorial has the following code to generate a queryset and send it to a template where it is consumed by OpenLayers.js:

tutorial's view.py:

def ward(request, id):
     ward = get_object_or_404(Ward, pk=id)
     interesting_points = InterestingLocation.objects.filter(
          geometry__intersects=(ward.geometry))
     return render_to_response("ward.html", { 
      'ward': ward, 'interesting_points': interesting_points }) 

tutorial's OpenLayers code (incomplete):

 geojson_format = new OpenLayers.Format.GeoJSON()
    ward = geojson_format.read({{ ward.geometry.geojson|safe}})[0];
    // We mark it 'safe' so that Django doesn't escape the quotes.

    ward.attributes = { 'name': "{{ward.name}}", 'type': 'ward'}; 
    vectors = new OpenLayers.Layer.Vector("Data");
    vectors.addFeatures(ward);

I have written the following code, but I keep getting the error message (js console) "Object has not method 'replace'".

my view.py

def interesting_area(request, iso3_id):
    iso3_id = iso3_id.upper()
    country = get_object_or_404(WorldBorder, iso3=iso3_id)
    interesting_points = InterestingLocation.objects.filter(
        geometry__intersects=(country.mpoly))
    return render_to_response("some_places.html", {
        'country': country,
        'interesting_points': interesting_points})

my openlayers.js attempt

function map_init() {
      json_format = new OpenLayers.Format.GeoJSON();
      countryson = json_format.read({{country.mpoly.geojson|safe}})[0];
      countryson.attributes = {'name': "{{country.name}}",
                            'area': "{{country.area}}",
                            'population': "{{country.pop2005}}",
                             'type': 'country'};
      vectors = new OpenLayers.Layer.Vector("Data");
      vectors.addFeatures(countryson);
      var map = new OpenLayers.Map('map-container');
      var base_layer = new OpenLayers.Layer.OSM("Base Map", {
                 "tileOptions": { "crossOriginKeyword": null } 
      });
      map.addLayers([base_layer, vectors]);
      map.zoomToMaxExtent(countryson.geometry.getBounds());
     }

I believe the error is in the line countryson = json_format.read({{country.mpoly.geojson|safe}})[0];

Does anyone know how to send out a model object and be able to have its geometry.geojson attribute be read on the template side? I already have seen how to do this by using a view/url that returns a static file, but I'd like to be able to do this by returning data directly to the template.

Footnote: I have seen a few other answers saying to use vectorformats, but it seems like there should be a way to do this natively in GeoDjango, but with my googling and searching for answers, I can't seem to find out how people usually do this.

Thanks for your help.

Edit:

I feel a bit foolish, but @sk1p asked me about the js traceback and when I looked at it, it told me that the line responsible for the error was the following:

map.zoomToMaxExtent(countryson.geometry.getBounds());

So I removed it and the error goes away, but I still cannot get my map to render. I will continue looking at the object returned.


Solution

  • After a lot of experimentation, I found a system that works to render geojson in the template. I think my error was pretty elementary and came from not understanding that the actual geometry field itself must be rendered as geojson (and this can be accessed from the template).

    I also switched to using Leaflet because it loaded really quickly and it seems to have a nice API.

    I have a program which imports shapefiles and breaks them up into Shapefile->Features->Attributes. This is inspired by the book Python for Geo-Spatial Development. The relevant models looks like this:

    models.py:

    class Shapefile(models.Model):
        filename = models.CharField(max_length=255)
        srs_wkt = models.TextField()
        geom_type = models.CharField(max_length=50)
    
    class Feature(models.Model):
        shapefile = models.ForeignKey(Shapefile)
        reference = models.CharField(max_length=100, blank=True)
        geom_point = models.PointField(srid=4326, blank=True, null=True)
        geom_multipoint = models.MultiPointField(srid=4326, blank=True, 
                                                 null=True)
        geom_multilinestring = models.MultiLineStringField(srid=4326, 
                                                           blank=True, 
                                                           null=True)
        geom_multipolygon = models.MultiPolygonField(srid=4326, 
                                                     blank=True, 
                                                     null=True)
        geom_geometrycollection = models.GeometryCollectionField(srid=4326,
                                                                 blank=True,
                                                                 null=True)
        objects = models.GeoManager()
    
        def __str__(self):
            return "{}".format(self.id)
    
        ## need some method to return the geometry type this guy has.
        def _get_geometry(self):
            geoms = [self.geom_point, self.geom_multipoint,
                     self.geom_multilinestring, self.geom_multipolygon,
                     self.geom_geometrycollection]
            geom = next(filter(lambda x: x, geoms))
            return geom
    
        geometry = property(_get_geometry)
    

    Note: because I am processing generic shapefiles and I do not previously know what kind of geometries the features will have, I have included geometry property on the feature to return the actual geometry field and discard the other unused ones. I use this later on to access the geometry's geojson method from within the template.

    In my view, I respond to requests that include a shapefile id with a query on the features in that shapefile.

    views.py:

    def view_shapefile(request, shapefile_id, 
                   template_file='shape_editor/viewshapefile.html'):
        all_features = Feature.objects.filter(
                         shapefile__id=shapefile_id).select_related(
                              'shapefile')
        filename = all_features[0].shapefile.filename
    
        ### we need something to set the center of our map ###
        lon, lat = all_features[0].geometry.centroid.tuple
        temp_vars = {'features' : all_features,
                     'filename' : filename,
                     'lon': lon,
                     'lat' : lat}
        return render(request, template_file, temp_vars) 
    

    And my template uses Leaflet to process the returned objects like so:

    function map_init(map_div) {
       var tileMapQuest = L.tileLayer('http://{s}.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.png', {
     subdomains: ['otile1','otile2','otile3','otile4'],
     attribution: 'Map tiles by <a href="http://open.mapquestapi.com/">MapQuest</a>. Data &copy; by <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>.',
     maxZoom: 18
       });
       var map_layers = [tileMapQuest];
       var map = L.map('map-container', {
         center: [{{ lat }}, {{ lon }}],
         zoom: 10,
         layers: map_layers,
         worldCopyJump: false
       });
       {% if features %}
         {% for feat in features %}
           var feature = new L.geoJson({{ feat.geometry.geojson|safe }}).addTo(map);
         {% endfor %}
       {% endif %}
    }
    

    I am not sure if that will be helpful for anyone else, and there are probably better ways to do it, particularly where I'm trying to figure out the latitude and longitude to use to center my maps.