1 import os
2 from django.contrib.gis.geos import Point, LinearRing, fromstr
3 from math import pi, sin, tan, sqrt, pow
4 from django.conf import settings
5 from django.db import connection
6 from django.core.cache import cache
7 from madrona.common.models import KmlCache
8 import zipfile
9 import re
10 import logging
11 import inspect
12 import tempfile
13
15 try:
16 fh = open(settings.LOG_FILE,'a')
17 logfile = settings.LOG_FILE
18 except:
19 import warnings
20 warnings.warn(" NOTICE: settings.LOG_FILE not specified or is not writeable; logging to stderr instead\n")
21 logfile = None
22
23 try:
24 level = settings.LOG_LEVEL
25 except AttributeError:
26 if settings.DEBUG:
27 level = logging.DEBUG
28 else:
29 level = logging.WARNING
30
31 format = '%(asctime)s %(name)s %(levelname)s %(message)s'
32 if logfile:
33 logging.basicConfig(level=level, format=format, filename=logfile)
34 else:
35 logging.basicConfig(level=level, format=format)
36
37 if not caller_name:
38 caller = inspect.currentframe().f_back
39 caller_name = caller.f_globals['__name__']
40
41 logger = logging.getLogger(caller_name)
42
43 if logfile and settings.DEBUG:
44 import sys
45 strm_out = logging.StreamHandler(sys.__stderr__)
46 logger.addHandler(strm_out)
47
48 return logger
49
50 log = get_logger()
51
53 return '<?xml version="1.0" encoding="UTF-8"?> <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">' + string + '</kml>'
54
55
57 lookAtParams = ComputeLookAt(geometry)
58 return '<LookAt><latitude>%f</latitude><longitude>%f</longitude><range>%f</range><tilt>%f</tilt><heading>%f</heading><altitudeMode>clampToGround</altitudeMode></LookAt>' % (lookAtParams['latitude'], lookAtParams['longitude'], lookAtParams['range'], lookAtParams['tilt'], lookAtParams['heading'])
59
61 """ takes a polygon or a multipolygon geometry and returns only the largest polygon geometry"""
62 if geom.num_geom > 1:
63 largest_area = 0.0
64 for g in geom:
65 if g.area > largest_area:
66 largest_geom = g
67 largest_area = g.area
68 else:
69 largest_geom = geom
70 return largest_geom
71
73 """ takes a line or a multiline geometry and returns only the longest line geometry"""
74 if geom.num_geom > 1:
75 largest_length = 0.0
76 for g in geom:
77 if g.length > largest_length:
78 largest_geom = g
79 largest_length = g.length
80 else:
81 largest_geom = geom
82 return largest_geom
83
84 -def angle(pnt1,pnt2,pnt3):
85 """
86 Return the angle in radians between line(pnt2,pnt1) and line(pnt2,pnt3)
87 """
88 cursor = connection.cursor()
89 if pnt1.srid:
90 query = "SELECT abs(ST_Azimuth(ST_PointFromText(\'%s\',%i), ST_PointFromText(\'%s\',%i) ) - ST_Azimuth(ST_PointFromText(\'%s\',%i), ST_PointFromText(\'%s\',%i)) )" % (pnt2.wkt,pnt2.srid,pnt1.wkt,pnt1.srid,pnt2.wkt,pnt2.srid,pnt3.wkt,pnt3.srid)
91 else:
92 query = "SELECT abs(ST_Azimuth(ST_PointFromText(\'%s\'), ST_PointFromText(\'%s\') ) - ST_Azimuth(ST_PointFromText(\'%s\'), ST_PointFromText(\'%s\')) )" % (pnt2.wkt,pnt1.wkt,pnt2.wkt,pnt3.wkt)
93
94 cursor.execute(query)
95 row = cursor.fetchone()
96 return row[0]
97
99 """
100 Return the angle in degrees between line(pnt2,pnt1) and line(pnt2,pnt3)
101 """
102 rads = angle(pnt1,pnt2,pnt3)
103 return rads * (180 / pi)
104
106 """
107 Returns a list of point indexes if ring contains spikes (angles of less than threshold degrees).
108 Otherwise, an empty list.
109 """
110 radian_thresh = threshold * (pi / 180)
111 spike_indecies = []
112 for i,pnt in enumerate(line_ring.coords):
113 if(i == 0 and line_ring.num_points > 3):
114 p1_coords = line_ring.coords[len(line_ring.coords) - 2]
115 elif(i == line_ring.num_points - 1):
116 break
117 else:
118 p1_coords = line_ring.coords[i - 1]
119
120
121 p1_str = 'POINT (%f %f), %i' % (p1_coords[0], p1_coords[1], settings.GEOMETRY_DB_SRID)
122 p1 = fromstr(p1_str)
123 p2_str = 'POINT (%f %f), %i' % (pnt[0],pnt[1],settings.GEOMETRY_DB_SRID)
124 p2 = fromstr(p2_str)
125 p3_coords = line_ring.coords[i + 1]
126 p3_str = 'POINT (%f %f), %i' % (p3_coords[0], p3_coords[1], settings.GEOMETRY_DB_SRID)
127 p3 = fromstr(p3_str)
128 if(angle(p1,p2,p3) <= radian_thresh):
129 spike_indecies.append(i)
130
131 return spike_indecies
132
134 """
135 Looks for spikes (angles < threshold degrees) in the polygons exterior ring. If there are spikes,
136 they will be removed and a polygon (without spikes) will be returned. If no spikes are found, method
137 will return original geometry.
138
139 NOTE: This method does not examine or fix interior rings. So far those haven't seemed to have been a problem.
140 """
141 line_ring = poly.exterior_ring
142 spike_indecies = spike_ring_indecies(line_ring,threshold=threshold)
143 if(spike_indecies):
144 for i,org_index in enumerate(spike_indecies):
145 if(org_index == 0):
146
147 pnts = list(line_ring.coords)
148
149 pnts.remove(pnts[0])
150
151 pnts.remove(pnts[-1])
152
153 pnts.append(pnts[0])
154
155 line_ring = LinearRing(pnts)
156 else:
157 line_ring.remove(line_ring.coords[org_index])
158 poly.exterior_ring = line_ring
159 return poly
160
162 """Send a geometry to the cleanGeometry stored procedure and get the cleaned geom back."""
163 cursor = connection.cursor()
164 query = "select cleangeometry(st_geomfromewkt(\'%s\')) as geometry" % geom.ewkt
165 cursor.execute(query)
166 row = cursor.fetchone()
167 newgeom = fromstr(row[0])
168
169 geometry = LargestPolyFromMulti(newgeom)
170
171 if not geometry.valid or (geometry.geom_type != 'Point' and geometry.num_coords < 2):
172 raise Exception("I can't clean this geometry. Dirty, filthy geometry. This geometry should be ashamed.")
173 else:
174 return geometry
175
176
177
178
179
180
182 old_srid = geo.srid
183 if geo.srid is not srid:
184 geo.transform(srid)
185 geo = clean_geometry(geo)
186 if not geo.valid:
187 raise Exception("ensure_clean could not produce a valid geometry.")
188 if geo.srid is not old_srid:
189 geo.transform(old_srid)
190 geo = clean_geometry(geo)
191 if not geo.valid:
192 raise Exception("ensure_clean could not produce a valid geometry.")
193 return geo
194
196
197 lookAtParams = {}
198
199 DEGREES = pi / 180.0
200 EARTH_RADIUS = 6378137.0
201
202 trans_geom = geometry.clone()
203 trans_geom.transform(settings.GEOMETRY_DB_SRID)
204
205 w = trans_geom.extent[0]
206 s = trans_geom.extent[1]
207 e = trans_geom.extent[2]
208 n = trans_geom.extent[3]
209
210 center_lon = trans_geom.centroid.y
211 center_lat = trans_geom.centroid.x
212
213 lngSpan = (Point(w, center_lat)).distance(Point(e, center_lat))
214 latSpan = (Point(center_lon, n)).distance(Point(center_lon, s))
215
216 aspectRatio = 1.0
217
218 PAD_FACTOR = 1.5
219
220 aspectUse = max(aspectRatio, min((lngSpan / latSpan),1.0))
221 alpha = (45.0 / (aspectUse + 0.4) - 2.0) * DEGREES
222
223
224 if lngSpan > latSpan:
225
226 beta = min(DEGREES * 90.0, alpha + lngSpan / 2.0 / EARTH_RADIUS)
227 else:
228
229 beta = min(DEGREES * 90.0, alpha + latSpan / 2.0 / EARTH_RADIUS)
230
231 lookAtParams['range'] = PAD_FACTOR * EARTH_RADIUS * (sin(beta) *
232 sqrt(1.0 / pow(tan(alpha),2.0) + 1.0) - 1.0)
233
234 trans_geom.transform(4326)
235
236 lookAtParams['latitude'] = trans_geom.centroid.y
237 lookAtParams['longitude'] = trans_geom.centroid.x
238 lookAtParams['tilt'] = 0
239 lookAtParams['heading'] = 0
240
241 return lookAtParams
242
248
250 from madrona.common import feedvalidator
251 from madrona.common.feedvalidator import compatibility
252 events = feedvalidator.validateString(kmlstring, firstOccurrenceOnly=1)['loggedEvents']
253
254
255
256
257
258 filterFunc = getattr(compatibility, "AA")
259 events = filterFunc(events)
260
261
262
263
264
265
266
267 events = [x for x in events if not (
268 (isinstance(x,feedvalidator.logging.UndefinedElement)
269 and x.params['element'] == u'ExtendedData') or
270 (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel)
271 and x.params['value'] == u'madrona.update_form') or
272 (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel)
273 and x.params['value'] == u'madrona.create_form') or
274 (isinstance(x,feedvalidator.logging.UnknownNamespace)
275 and x.params['namespace'] == u'http://madrona.org') or
276 (isinstance(x,feedvalidator.logging.UnknownNamespace)
277 and x.params['namespace'] == u'http://www.google.com/kml/ext/2.2') or
278 (isinstance(x,feedvalidator.logging.InvalidItemIconState)
279 and x.params['element'] == u'state' and ' ' in x.params['value']) or
280 (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel)
281 and x.params['element'] == u'atom:link' and 'workspace' in x.params['value'])
282 )]
283
284 from madrona.common.feedvalidator.formatter.text_plain import Formatter
285 output = Formatter(events)
286
287 if output:
288 errors = []
289 for i in range(len(output)):
290 errors.append((events[i],events[i].params,output[i],kmlstring.splitlines()[events[i].params['backupline']]))
291 return errors
292 else:
293 return None
294
296 """
297 Takes an 8 digit hex color string (used by Google Earth) and converts it to RGBA colorspace
298 * 8-digit hex codes use AABBGGRR (R - red, G - green, B - blue, A - alpha transparency)
299 """
300 hex8 = str(hex8.replace('#',''))
301 if len(hex8) != 8:
302 raise Exception("Hex8 value must be exactly 8 digits")
303 hex_values = [hex8[i:i + 2:1] for i in xrange(0, len(hex8), 2)]
304 rgba_values = [int(x,16) for x in hex_values]
305 rgba_values.reverse()
306 return rgba_values
307
308 from django.utils.importlib import import_module
309
311 if session_key and session_key != '0':
312 engine = import_module(settings.SESSION_ENGINE)
313 request.session = engine.SessionStore(session_key)
314
316 """
317 Returns boolean depending on whether we support their browser
318 based on their HTTP_USER_AGENT
319
320 Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7
321 Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-us) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10
322 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b7) Gecko/20100101 Firefox/4.0b7
323 """
324 supported_browsers = [
325 ('Firefox', 3, 5, 'Mac'),
326 ('Firefox', 4, 0, 'Mac'),
327 ('Safari', 3, 1, 'Mac'),
328 ('Chrome', 6, 0, 'Mac'),
329 ('Firefox', 3, 5, 'Windows'),
330 ('Firefox', 4, 0, 'Windows'),
331 ('Chrome', 1, 0, 'Windows'),
332 ('IE', 8, 0, 'Windows'),
333 ]
334
335 from madrona.common import uaparser
336
337 bp = uaparser.browser_platform(ua)
338 if not bp.platform:
339 log.warn("Platform is None: UA String is '%s'" % ua)
340
341 for sb in supported_browsers:
342 if bp.family == sb[0] and \
343 ((bp.v1 == sb[1] and bp.v2 >= sb[2]) or bp.v1 > sb[1]) and \
344 bp.platform == sb[3]:
345 return True
346
347 return False
348
350 """
351 Recursively adds a directory to a zipfile
352 modified from http://stackoverflow.com/questions/458436/adding-folders-to-a-zip-file-using-python
353
354 from madrona.common.utils import ZipUtil
355 zu = ZipUtil()
356 filename = 'TEMP.zip'
357 directory = 'kmldir' # containing doc.kml, etc
358 zu.toZip(directory, filename)
359 """
360 - def toZip(self, file, filename):
361 zip_file = zipfile.ZipFile(filename, 'w')
362 if os.path.isfile(file):
363 zip_file.write(file)
364 else:
365 self.addFolderToZip(zip_file, file)
366 zip_file.close()
367
369 if not folder or folder == '':
370 folder_path = '.'
371 else:
372 folder_path = folder
373
374
375 doc = os.path.join(folder,'doc.kml')
376 if os.path.exists(doc):
377
378 zip_file.write(doc)
379
380 for file in os.listdir(folder_path):
381 full_path = os.path.join(folder, file)
382 if os.path.isfile(full_path) and not full_path.endswith("doc.kml"):
383
384 zip_file.write(full_path)
385 elif os.path.isdir(full_path):
386
387 self.addFolderToZip(zip_file, full_path)
388
390 """
391 Determines if a LinearRing is oriented counter-clockwise or not
392 """
393 area = 0.0
394 for i in range(0,len(ring) - 1):
395 p1 = ring[i]
396 p2 = ring[i + 1]
397 area += (p1[1] * p2[0]) - (p1[0] * p2[1])
398
399 if area > 0:
400 return False
401 else:
402 return True
403
404
405 from django.contrib.gis.geos import Polygon
407 """
408 reverses rings so that polygon follows the Right-hand rule
409 exterior ring = clockwise
410 interior rings = counter-clockwise
411 """
412 assert polygon.geom_type == 'Polygon'
413 if polygon.empty:
414 return poly
415 exterior = True
416 rings = []
417 for ring in polygon:
418 assert ring.ring
419 if exterior:
420 if isCCW(ring):
421 ring.reverse()
422 exterior = False
423 else:
424 if not isCCW(ring):
425 ring.reverse()
426 rings.append(ring)
427 poly = Polygon(*rings)
428 return poly
429
431 """
432 reverses rings so that geometry complies with the LEFT-hand rule
433 Google Earth KML requires this oddity
434 exterior ring = counter-clockwise
435 interior rings = clockwise
436 """
437 assert polygon.geom_type == 'Polygon'
438 assert not polygon.empty
439 exterior = True
440 rings = []
441 for ring in polygon:
442 assert ring.ring
443 if exterior:
444 if not isCCW(ring):
445 ring.reverse()
446 exterior = False
447 else:
448 if isCCW(ring):
449 ring.reverse()
450 rings.append(ring)
451 poly = Polygon(*rings)
452 return poly
453
454 -def asKml(input_geom, altitudeMode=None, uid=''):
455 """
456 Performs three critical functions for creating suitable KML geometries:
457 - simplifies the geoms (lines, polygons only)
458 - forces left-hand rule orientation
459 - sets the altitudeMode shape
460 (usually one of: absolute, clampToGround, relativeToGround)
461 """
462 if altitudeMode is None:
463 try:
464 altitudeMode = settings.KML_ALTITUDEMODE_DEFAULT
465 except:
466 altitudeMode = None
467
468 key = "asKml_%s_%s_%s" % (input_geom.wkt.__hash__(), altitudeMode, uid)
469 kmlcache, created = KmlCache.objects.get_or_create(key=key)
470 kml = kmlcache.kml_text
471 if not created and kml:
472 return kml
473
474 log.debug("%s ...no kml cache found...seeding" % key)
475
476 latlon_geom = input_geom.transform(4326, clone=True)
477
478 if latlon_geom.geom_type in ['Polygon','LineString']:
479 geom = latlon_geom.simplify(settings.KML_SIMPLIFY_TOLERANCE_DEGREES)
480
481
482 if geom.empty or not geom.valid:
483 toler = settings.KML_SIMPLIFY_TOLERANCE_DEGREES
484 maxruns = 20
485 for i in range(maxruns):
486 toler = toler / 3.0
487 geom = latlon_geom.simplify(toler)
488 log.debug("%s ... Simplification failed ... tolerance=%s" % (key,toler))
489 if not geom.empty and geom.valid:
490 break
491 if i == maxruns - 1:
492 geom = latlon_geom
493 else:
494 geom = latlon_geom
495
496 if geom.geom_type == 'Polygon':
497 geom = forceLHR(geom)
498
499 kml = geom.kml
500
501 if altitudeMode and geom.geom_type == 'Polygon':
502 kml = kml.replace('<Polygon>', '<Polygon><altitudeMode>%s</altitudeMode><extrude>1</extrude>' % altitudeMode)
503
504 kml = kml.replace(',0 ', ',%s ' % settings.KML_EXTRUDE_HEIGHT)
505
506 kmlcache.kml_text = kml
507 kmlcache.save()
508 return kml
509
511 """
512 Give group permission to share models
513 Permissions are attached to models but we want this perm to be 'global'
514 Fake it by attaching the perm to the Group model (from the auth app)
515 We check for this perm like: user1.has_perm("auth.can_share_features")
516 """
517 from django.contrib.auth.models import Permission, Group
518 from django.contrib.contenttypes.models import ContentType
519
520 try:
521 p = Permission.objects.get(codename='can_share_features')
522 except Permission.DoesNotExist:
523 gct = ContentType.objects.get(name="group")
524 p = Permission.objects.create(codename='can_share_features',name='Can Share Features',content_type=gct)
525 p.save()
526
527
528 for groupname in settings.SHARING_TO_PUBLIC_GROUPS:
529 g, created = Group.objects.get_or_create(name=groupname)
530 g.permissions.add(p)
531 g.save()
532
533 for groupname in settings.SHARING_TO_STAFF_GROUPS:
534 g, created = Group.objects.get_or_create(name=groupname)
535 g.permissions.add(p)
536 g.save()
537
538 if group:
539
540 group.permissions.add(p)
541 group.save()
542 return True
543
544
545 '''
546 Returns a path to desired resource (image file)
547 Called from within pisaDocument via link_callback parameter (from pdf_report)
548 '''
550 import os
551 import settings
552 import datetime
553 import random
554 import tempfile
555 import urllib2
556 from django.test.client import Client
557
558 if uri.startswith('http'):
559
560 req = urllib2.Request(uri)
561 response = urllib2.urlopen(req)
562 content = response.read()
563 elif 'staticmap' in uri:
564
565 from madrona.staticmap.temp_save import img_from_params
566 params = get_params_from_uri(uri)
567 content = img_from_params(params, None)
568 else:
569
570 client = Client()
571 response = client.get(uri)
572 content = response.content
573
574
575
576 randnum = random.randint(0, 1000000000)
577 timestamp = datetime.datetime.now().strftime('%m_%d_%y_%H%M')
578 filename = 'resource_%s_%s.tmp' % (timestamp,randnum)
579 pathname = os.path.join(tempfile.gettempdir(),filename)
580 fh = open(pathname,'wb')
581 fh.write(content)
582 fh.close()
583 return pathname
584
585 '''
586 Returns a dictionary representation of the parameters attached to the given uri
587 Called by fetch_resources
588 '''
590 from urlparse import urlparse
591 results = urlparse(uri)
592 params = {}
593 if results.query == '':
594 return params
595 params_list = results.query.split('&')
596 for param in params_list:
597 pair = param.split('=')
598 params[pair[0]] = pair[1]
599 return params
600
602 """
603 Tests a string to see if it's binary
604 borrowed from http://code.activestate.com/recipes/173220-test-if-a-file-or-string-is-text-or-binary/
605 """
606 import string
607 from django.utils.encoding import smart_str
608 s = smart_str(s)
609 text_characters = "".join(map(chr, range(32, 127)) + list("\n\r\t\b"))
610 _null_trans = string.maketrans("", "")
611
612 if "\0" in s:
613 return False
614 if not s:
615 return True
616
617
618
619 t = s.translate(_null_trans, text_characters)
620
621
622
623 if float(len(t)) / len(s) > 0.30:
624 return False
625 return True
626
627 from django.core.cache import cache
629 '''
630 http://djangosnippets.org/snippets/1130/
631 Cacheable class method decorator
632 from madrona.common.utils import cachemethod
633 @cachemethod("SomeClass_get_some_result_%(id)s")
634 '''
635 def paramed_decorator(func):
636 def decorated(self):
637 key = cache_key % self.__dict__
638 res = cache.get(key)
639 if res == None:
640 res = func(self)
641 cache.set(key, res, timeout)
642 return res
643 decorated.__doc__ = func.__doc__
644 decorated.__dict__ = func.__dict__
645 return decorated
646 return paramed_decorator
647