1 """
2 Generate StyledLayerDescriptor objecs for django querysets.
3
4 This generator uses the python-sld and pysal libraries to generate classes
5 for map classification, and returns a StyledLayerDescriptor class object. This
6 class object may be serialized to an SLD XML file, which is useful for many
7 GIS and mapping software packages.
8
9 License
10 =======
11 Copyright 2011-2012 David Zwarg <U{dzwarg@azavea.com}>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 U{http://www.apache.org/licenses/LICENSE-2.0}
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 @author: David Zwarg
26 @contact: dzwarg@azavea.com
27 @copyright: 2011-2012, Azavea
28 @license: Apache 2.0
29 @version: 1.0.7
30 """
31
32 from sld import *
33 from numpy import array, ndarray
34 from pysal.esda.mapclassify import *
35 from django.contrib.gis.db.models import fields
36
38 """
39 Generate equal interval classes from the provided queryset. If the queryset
40 is empty, no class breaks are returned. For more information on the Equal
41 Interval classifier, please visit:
42
43 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Equal_Interval}
44
45 @type queryset: QuerySet
46 @param queryset: The query set that contains the entire distribution of
47 data values.
48 @type field: string
49 @param field: The name of the field on the model in the queryset that
50 contains the data values.
51 @type nclasses: integer
52 @param nclasses: The number of class breaks desired.
53 @type geofield: string
54 @param geofield: The name of the geometry field. Defaults to 'geom'.
55 @rtype: L{sld.StyledLayerDescriptor}
56 @returns: An SLD object that represents the class breaks.
57 """
58 return _as_classification(Equal_Interval, *args, **kwargs)
59
61 """
62 Generate Fisher-Jenks classes from the provided queryset. If the queryset
63 is empty, no class breaks are returned. For more information on the Fisher
64 Jenks classifier, please visit:
65
66 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Fisher_Jenks}
67
68 @type queryset: QuerySet
69 @param queryset: The query set that contains the entire distribution of
70 data values.
71 @type field: string
72 @param field: The name of the field on the model in the queryset that
73 contains the data values.
74 @type nclasses: integer
75 @param nclasses: The number of class breaks desired.
76 @type geofield: string
77 @param geofield: The name of the geometry field. Defaults to 'geom'.
78 @rtype: L{sld.StyledLayerDescriptor}
79 @returns: An SLD object that represents the class breaks.
80 """
81 return _as_classification(Fisher_Jenks, *args, **kwargs)
82
84 """
85 Generate Jenks-Caspall classes from the provided queryset. If the queryset
86 is empty, no class breaks are returned. For more information on the Jenks
87 Caspall classifier, please visit:
88
89 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Jenks_Caspall}
90
91 @type queryset: QuerySet
92 @param queryset: The query set that contains the entire distribution of
93 data values.
94 @type field: string
95 @param field: The name of the field on the model in the queryset that
96 contains the data values.
97 @type nclasses: integer
98 @param nclasses: The number of class breaks desired.
99 @type geofield: string
100 @param geofield: The name of the geometry field. Defaults to 'geom'.
101 @rtype: L{sld.StyledLayerDescriptor}
102 @returns: An SLD object that represents the class breaks.
103 """
104 return _as_classification(Jenks_Caspall, *args, **kwargs)
105
107 """
108 Generate Jenks-Caspall Forced classes from the provided queryset. If the queryset
109 is empty, no class breaks are returned. For more information on the Jenks
110 Caspall Forced classifier, please visit:
111
112 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Jenks_Caspall_Forced}
113
114 @type queryset: QuerySet
115 @param queryset: The query set that contains the entire distribution of
116 data values.
117 @type field: string
118 @param field: The name of the field on the model in the queryset that
119 contains the data values.
120 @type nclasses: integer
121 @param nclasses: The number of class breaks desired.
122 @type geofield: string
123 @param geofield: The name of the geometry field. Defaults to 'geom'.
124 @rtype: L{sld.StyledLayerDescriptor}
125 @returns: An SLD object that represents the class breaks.
126 """
127 return _as_classification(Jenks_Caspall_Forced, *args, **kwargs)
128
130 """
131 Generate Jenks-Caspall Sampled classes from the provided queryset. If the queryset
132 is empty, no class breaks are returned. For more information on the Jenks
133 Caspall Sampled classifier, please visit:
134
135 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Jenks_Caspall_Sampled}
136
137 @type queryset: QuerySet
138 @param queryset: The query set that contains the entire distribution of
139 data values.
140 @type field: string
141 @param field: The name of the field on the model in the queryset that
142 contains the data values.
143 @type nclasses: integer
144 @param nclasses: The number of class breaks desired.
145 @type geofield: string
146 @param geofield: The name of the geometry field. Defaults to 'geom'.
147 @rtype: L{sld.StyledLayerDescriptor}
148 @returns: An SLD object that represents the class breaks.
149 """
150 return _as_classification(Jenks_Caspall_Sampled, *args, **kwargs)
151
153 """
154 Generate Max P classes from the provided queryset. If the queryset
155 is empty, no class breaks are returned. For more information on the Max P
156 classifier, please visit:
157
158 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Max_P_Classifier}
159
160 @type queryset: QuerySet
161 @param queryset: The query set that contains the entire distribution of
162 data values.
163 @type field: string
164 @param field: The name of the field on the model in the queryset that
165 contains the data values.
166 @type nclasses: integer
167 @param nclasses: The number of class breaks desired.
168 @type geofield: string
169 @param geofield: The name of the geometry field. Defaults to 'geom'.
170 @rtype: L{sld.StyledLayerDescriptor}
171 @returns: An SLD object that represents the class breaks.
172 """
173 return _as_classification(Max_P_Classifier, *args, **kwargs)
174
176 """
177 Generate Maximum Breaks classes from the provided queryset. If the queryset
178 is empty, no class breaks are returned. For more information on the Maximum
179 Breaks classifier, please visit:
180
181 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Maximum_Breaks}
182
183 @type queryset: QuerySet
184 @param queryset: The query set that contains the entire distribution of
185 data values.
186 @type field: string
187 @param field: The name of the field on the model in the queryset that
188 contains the data values.
189 @type nclasses: integer
190 @param nclasses: The number of class breaks desired.
191 @type geofield: string
192 @param geofield: The name of the geometry field. Defaults to 'geom'.
193 @rtype: L{sld.StyledLayerDescriptor}
194 @returns: An SLD object that represents the class breaks.
195 """
196 return _as_classification(Maximum_Breaks, *args, **kwargs)
197
199 """
200 Generate Natural Breaks classes from the provided queryset. If the queryset
201 is empty, no class breaks are returned. For more information on the Natural
202 Breaks classifier, please visit:
203
204 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Natural_Breaks}
205
206 @type queryset: QuerySet
207 @param queryset: The query set that contains the entire distribution of
208 data values.
209 @type field: string
210 @param field: The name of the field on the model in the queryset that
211 contains the data values.
212 @type nclasses: integer
213 @param nclasses: The number of class breaks desired.
214 @type geofield: string
215 @param geofield: The name of the geometry field. Defaults to 'geom'.
216 @rtype: L{sld.StyledLayerDescriptor}
217 @returns: An SLD object that represents the class breaks.
218 """
219 return _as_classification(Natural_Breaks, *args, **kwargs)
220
222 """
223 Generate Quantile classes from the provided queryset. If the queryset
224 is empty, no class breaks are returned. For more information on the Quantile
225 classifier, please visit:
226
227 U{http://pysal.geodacenter.org/1.2/library/esda/mapclassify.html#pysal.esda.mapclassify.Quantiles}
228
229 @type queryset: QuerySet
230 @param queryset: The query set that contains the entire distribution of
231 data values.
232 @type field: string
233 @param field: The name of the field on the model in the queryset that
234 contains the data values.
235 @type nclasses: integer
236 @param nclasses: The number of class breaks desired.
237 @type geofield: string
238 @param geofield: The name of the geometry field. Defaults to 'geom'.
239 @rtype: L{sld.StyledLayerDescriptor}
240 @returns: An SLD object that represents the class breaks.
241 """
242 return _as_classification(Quantiles, *args, **kwargs)
243
244 -def _as_classification(classification, queryset, field, nclasses, geofield='geom',
245 propertyname=None, userstyletitle=None, featuretypestylename=None, colorbrewername='',
246 invertgradient=False, **kwargs):
247 """
248 Accept a queryset of objects, and return the values of the class breaks
249 on the data distribution. If the queryset is empty, no class breaks are
250 computed.
251
252 @type classification: pysal classifier
253 @param classification: A classification class defined in
254 pysal.esda.mapclassify. As of version 1.0.1, this list is comprised of:
255
256 - Equal_Interval
257 - Fisher_Jenks
258 - Jenks_Caspall
259 - Jenks_Caspall_Forced
260 - Jenks_Caspall_Sampled
261 - Max_P_Classifier
262 - Maximum_Breaks
263 - Natural_Breaks
264 - Quantiles
265
266 @type queryset: QuerySet
267 @param queryset: The query set that contains the entire distribution of data values.
268 @type field: string
269 @param field: The name of the field on the model in the queryset that contains the data values.
270 @type nclasses: integer
271 @param nclasses: The number of class breaks desired.
272 @type geofield: string
273 @keyword geofield: The name of the geography column on the model. Defaults to 'geom'
274 @type propertyname: string
275 @keyword propertyname: The name of the filter property name, if different from the model field.
276 @type userstyletitle: string
277 @keyword userstyletitle: The title of the UserStyle element.
278 @type featuretypestylename: string
279 @keyword featuretypestylename: The name of the FeatureTypeStyle element.
280 @type colorbrewername: string
281 @keyword colorbrewername: The name of a colorbrewer ramp name. Must have the same # of corresponding classes as nclasses.
282 @type invertgradient: boolean
283 @keyword invertgradient: Should the resulting SLD have colors from high to low, instead of low to high?
284 @type kwargs: keywords
285 @param kwargs: Additional keyword arguments for the classifier.
286 @rtype: L{sld.StyledLayerDescriptor}
287 @returns: An SLD class object that represents the classification scheme
288 and filters.
289 """
290 thesld = StyledLayerDescriptor()
291
292 ftype = queryset.model._meta.get_field_by_name(geofield)[0]
293 if isinstance(ftype, fields.LineStringField) or isinstance(ftype, fields.MultiLineStringField):
294 symbolizer = LineSymbolizer
295 elif isinstance(ftype, fields.PolygonField) or isinstance(ftype, fields.MultiPolygonField):
296 symbolizer = PolygonSymbolizer
297 else:
298
299 symbolizer = PointSymbolizer
300
301 if propertyname is None:
302 propertyname = field
303
304 nl = thesld.create_namedlayer('%d breaks on "%s" as %s' % (nclasses, field, classification.__name__))
305 us = nl.create_userstyle()
306 if not userstyletitle is None:
307 us.Title = str(userstyletitle)
308 fts = us.create_featuretypestyle()
309 if not featuretypestylename is None:
310 fts.Name = str(featuretypestylename)
311
312
313 if nclasses == 1:
314 rule = fts.create_rule(propertyname, symbolizer=symbolizer)
315 shade = 0 if invertgradient else 255
316 shade = '#%02x%02x%02x' % (shade, shade, shade,)
317
318
319 if symbolizer == PointSymbolizer:
320 rule.PointSymbolizer.Graphic.Mark.Fill.CssParameters[0].Value = shade
321 elif symbolizer == LineSymbolizer:
322 rule.LineSymbolizer.Stroke.CssParameters[0].Value = shade
323 elif symbolizer == PolygonSymbolizer:
324 rule.PolygonSymbolizer.Stroke.CssParameters[0].Value = '#000000'
325 rule.PolygonSymbolizer.Fill.CssParameters[0].Value = shade
326
327 thesld.normalize()
328
329 return thesld
330
331
332 datavalues = array(queryset.order_by(field).values_list(field, flat=True))
333 q = classification(datavalues, nclasses, **kwargs)
334
335 shades = None
336 if q.k == nclasses and colorbrewername and not colorbrewername == '':
337 try:
338 import colorbrewer
339 shades = getattr(colorbrewer, colorbrewername)[nclasses]
340
341 if invertgradient:
342 shades.reverse()
343 except:
344
345 pass
346
347 for i,qbin in enumerate(q.bins):
348 if type(qbin) == ndarray:
349 qbin = qbin[0]
350
351 title = '<= %s' % qbin
352 rule = fts.create_rule(title, symbolizer=symbolizer)
353
354 if shades:
355 shade = '#%02x%02x%02x' % shades[i]
356 else:
357 shade = (float(q.k - i) / q.k ) * 255
358 if invertgradient:
359 shade = 255 - shade
360 shade = '#%02x%02x%02x' % (shade, shade, shade,)
361
362 if symbolizer == PointSymbolizer:
363 rule.PointSymbolizer.Graphic.Mark.Fill.CssParameters[0].Value = shade
364 elif symbolizer == LineSymbolizer:
365 rule.LineSymbolizer.Stroke.CssParameters[0].Value = shade
366 elif symbolizer == PolygonSymbolizer:
367 rule.PolygonSymbolizer.Stroke.CssParameters[0].Value = '#000000'
368 rule.PolygonSymbolizer.Fill.CssParameters[0].Value = shade
369
370
371 if i > 0:
372 f_low = Filter(rule)
373 f_low.PropertyIsGreaterThan = PropertyCriterion(f_low, 'PropertyIsGreaterThan')
374 f_low.PropertyIsGreaterThan.PropertyName = propertyname
375 f_low.PropertyIsGreaterThan.Literal = str(q.bins[i-1])
376
377 f_high = Filter(rule)
378 f_high.PropertyIsLessThanOrEqualTo = PropertyCriterion(f_high, 'PropertyIsLessThanOrEqualTo')
379 f_high.PropertyIsLessThanOrEqualTo.PropertyName = propertyname
380 f_high.PropertyIsLessThanOrEqualTo.Literal = str(qbin)
381
382
383 if i > 0:
384 rule.Filter = f_low + f_high
385 else:
386 rule.Filter = f_high
387
388 thesld.normalize()
389
390 return thesld
391