Skip to content

Commit

Permalink
Support GeoJSON/KML for multiple areas.
Browse files Browse the repository at this point in the history
Fixes #136.
  • Loading branch information
chris48s authored and dracos committed Sep 18, 2015
1 parent 52edb97 commit 603c857
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 75 deletions.
139 changes: 139 additions & 0 deletions mapit/geometryserialiser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import json

from django.conf import settings
from django.contrib.gis.gdal import OGRException, SRSException
from django.utils.html import escape


class TransformError(Exception):
pass


# serialise a list of Area objects into .kml .geojson format
# .wkt is supported only for a list of length 1
class GeometrySerialiser(object):
kml_header =\
"""<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Style id="ourPolygonStyle">
<LineStyle>
<color>%s</color>
<width>2</width>
</LineStyle>
<PolyStyle>
<color>%s</color>
</PolyStyle>
</Style>"""
kml_placemark =\
"""
<Placemark>
<styleUrl>#ourPolygonStyle</styleUrl>
<name>%s</name>
%s
</Placemark>"""
kml_footer =\
"""
</Document>
</kml>"""

def __init__(self, areas, srid, simplify_tolerance):
self.areas = areas
self.srid = srid
self.simplify_tolerance = simplify_tolerance

# collect all polygons that make up an area
def __collect_polygons(self, area):
all_polygons = area.polygons.all()
if len(all_polygons) > 1:
all_polygons = all_polygons.collect()
elif len(all_polygons) == 1:
all_polygons = all_polygons[0].polygon
else:
return None
return all_polygons

# transform to a different co-ordinate system
def __transform(self, polygons):
if self.srid != settings.MAPIT_AREA_SRID:
try:
polygons.transform(self.srid)
except (SRSException, OGRException) as e:
raise TransformError("Error with transform: %s" % e)
return polygons

# apply boundary simplification
def __simplify(self, polygons, name):
num_points_before_simplification = polygons.num_points
if self.simplify_tolerance:
polygons = polygons.simplify(self.simplify_tolerance)
if polygons.num_points == 0 and num_points_before_simplification > 0:
raise TransformError("Simplifying %s with tolerance %f left no boundary at all" % (
name, self.simplify_tolerance))
return polygons

# apply pre-processing to self.areas before serialisation
def __process_polygons(self):
processed_areas = []
for area in self.areas:
polygons = self.__collect_polygons(area)
if polygons:
polygons = self.__transform(polygons)
polygons = self.__simplify(polygons, area.name)
processed_areas.append((polygons, area))
if len(processed_areas) == 0:
raise TransformError("No polygons found")
else:
return processed_areas

# output self.areas as kml
def kml(self, kml_type, line_colour="70ff0000", fill_colour="3dff5500"):
content_type = 'application/vnd.google-earth.kml+xml'
processed_areas = self.__process_polygons()

if kml_type == "full":
output = self.kml_header % (line_colour, fill_colour)
for area in processed_areas:
output += self.kml_placemark % (escape(area[1].name), area[0].kml)
output += self.kml_footer
return (output, content_type)
elif kml_type == "polygon":
if len(processed_areas) == 1:
return (processed_areas[0][0].kml, content_type)
else:
raise Exception("kml_type: '%s' not supported for multiple areas"
% (kml_type,))
else:
raise Exception("Unknown kml_type: '%s'" % (kml_type,))

# output self.areas as geojson
def geojson(self):
content_type = 'application/json'
processed_areas = self.__process_polygons()
if len(processed_areas) == 1:
return (processed_areas[0][0].json, content_type)
else:
output = {
'type': 'FeatureCollection',
'features': [
self.area_as_geojson_feature(area[1], area[0])
for area in processed_areas
]
}
return (json.dumps(output), content_type)

def area_as_geojson_feature(self, area, polygons):
return {
'type': 'Feature',
'properties': {'name': area.name},
'geometry': json.loads(polygons.json),
}

# output self.areas as wkt
def wkt(self):
content_type = 'text/plain'
processed_areas = self.__process_polygons()
if len(processed_areas) == 1:
return (processed_areas[0][0].wkt, content_type)
else:
raise Exception("wkt not supported for multiple areas")
3 changes: 2 additions & 1 deletion mapit/management/commands/mapit_make_fusion_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
import colorsys

from django.core.management.base import BaseCommand
from mapit.models import Area, Generation, TransformError
from mapit.models import Area, Generation
from mapit.geometryserialiser import TransformError


def hsv_to_rgb(h, s, v):
Expand Down
63 changes: 8 additions & 55 deletions mapit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
import itertools

from django.contrib.gis.db import models
from django.contrib.gis.gdal import SRSException, OGRException
from django.conf import settings
from django.db import connection
from django.db.models.query import RawQuerySet
from django.utils.html import escape
from django.utils.encoding import python_2_unicode_compatible

from mapit.managers import Manager, GeoManager
from mapit import countries
from mapit.djangopatch import GetQuerySetMetaclass
from mapit.geometryserialiser import GeometrySerialiser
from django.utils import six


Expand Down Expand Up @@ -222,10 +221,6 @@ def get_or_create_with_code(self, country=None, type=None, code_type='', code=''
return area


class TransformError(Exception):
pass


@python_2_unicode_compatible
class Area(models.Model):
name = models.CharField(max_length=2000, blank=True)
Expand Down Expand Up @@ -318,59 +313,17 @@ def export(self,
something else goes wrong with the spatial transform, then a
TransformError exception is raised.
"""
all_areas = self.polygons.all()
if len(all_areas) > 1:
all_areas = all_areas.collect()
elif len(all_areas) == 1:
all_areas = all_areas[0].polygon
else:
all_polygons = self.polygons.all()
if len(all_polygons) == 0:
return (None, None)

if srid != settings.MAPIT_AREA_SRID:
try:
all_areas.transform(srid)
except (SRSException, OGRException) as e:
raise TransformError("Error with transform: %s" % e)

num_points_before_simplification = all_areas.num_points
if simplify_tolerance:
all_areas = all_areas.simplify(simplify_tolerance)
if all_areas.num_points == 0 and num_points_before_simplification > 0:
raise TransformError("Simplifying %s with tolerance %f left no boundary at all" % (
self, simplify_tolerance))

areas = [self]
serialiser = GeometrySerialiser(areas, srid, simplify_tolerance)
if export_format == 'kml':
if kml_type == "polygon":
out = all_areas.kml
elif kml_type == "full":
out = '''<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Style id="ourPolygonStyle">
<LineStyle>
<color>%s</color>
<width>2</width>
</LineStyle>
<PolyStyle>
<color>%s</color>
</PolyStyle>
</Style>
<Placemark>
<styleUrl>#ourPolygonStyle</styleUrl>
<name>%s</name>
%s
</Placemark>
</Document>
</kml>''' % (line_colour, fill_colour, escape(self.name), all_areas.kml)
else:
raise Exception("Unknown kml_type: '%s'" % (kml_type,))
content_type = 'application/vnd.google-earth.kml+xml'
out, content_type = serialiser.kml(kml_type, line_colour, fill_colour)
elif export_format in ('json', 'geojson'):
out = all_areas.json
content_type = 'application/json'
out, content_type = serialiser.geojson()
elif export_format == 'wkt':
out = all_areas.wkt
content_type = 'text/plain'
out, content_type = serialiser.wkt()
return (out, content_type)


Expand Down
8 changes: 8 additions & 0 deletions mapit/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ def output_json(out, code=200):
return response


def output_polygon(content_type, output):
response = http.HttpResponse(content_type='%s; charset=utf-8' % content_type)
response['Access-Control-Allow-Origin'] = '*'
response['Cache-Control'] = 'max-age=2419200' # 4 weeks
response.write(output)
return response


def get_object_or_404(klass, format='json', *args, **kwargs):
try:
return orig_get_object_or_404(klass, *args, **kwargs)
Expand Down
34 changes: 34 additions & 0 deletions mapit/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,37 @@ def test_nearest_with_bad_srid(self):
self.assertEqual(content, {
'code': 400, 'error': 'GetProj4StringSPI: Cannot find SRID (84) in spatial_ref_sys\n'
})

def test_areas_polygon_valid(self):
id1 = self.small_area_1.id
id2 = self.small_area_2.id
url = '/areas/%d,%d.geojson' % (id1, id2)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(len(content), 2)

def test_areas_polygon_one_id(self):
id = self.small_area_1.id

url_area = '/area/%d.geojson' % id
response_area = self.client.get(url_area)
self.assertEqual(response_area.status_code, 200)
content_area = json.loads(response_area.content.decode('utf-8'))

url_areas = '/areas/%d.geojson' % id
response_areas = self.client.get(url_areas)
self.assertEqual(response_areas.status_code, 200)
content_areas = json.loads(response_areas.content.decode('utf-8'))

self.assertEqual(content_area, content_areas)

def test_areas_polygon_bad_params(self):
url = '/areas/99999.geojson'
response = self.client.get(url)
self.assertEqual(response.status_code, 404)

id = self.small_area_1.id
url = '/areas/%d.geojson?simplify_tolerance=a' % id
response = self.client.get(url)
self.assertEqual(response.status_code, 400)
2 changes: 2 additions & 0 deletions mapit/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
url(r'^nearest/(?P<srid>[0-9]+)/(?P<x>%s),(?P<y>%s)%s$' % (number, number, format_end), postcodes.nearest),

url(r'^areas/(?P<area_ids>[0-9]+(?:,[0-9]+)*)%s$' % format_end, areas.areas),
url(r'^areas/(?P<area_ids>[0-9]+(?:,[0-9]+)*)\.(?P<format>kml|geojson)$', areas.areas_polygon),
url(r'^areas/(?P<srid>[0-9]+)/(?P<area_ids>[0-9]+(?:,[0-9]+)*)\.(?P<format>kml|geojson)$', areas.areas_polygon),
url(r'^areas/(?P<area_ids>[0-9]+(?:,[0-9]+)*)/geometry$', areas.areas_geometry),
url(r'^areas/(?P<type>[A-Z0-9,]*[A-Z0-9]+)%s$' % format_end, areas.areas_by_type),
url(r'^areas/(?P<name>.+?)%s$' % format_end, areas.areas_by_name),
Expand Down
Loading

0 comments on commit 603c857

Please sign in to comment.