source: src/django_gheat/website/tile.py@ 9222

Last change on this file since 9222 was 9188, checked in by rick, 14 years ago

Show-case cache generation to avoid re-generation the pictures all the time.

  • Property svn:executable set to *
File size: 9.9 KB
RevLine 
[9147]1#!/usr/bin/env python
2#
[9150]3# Hack to show image generation realtime, sample tile server implementation.
[9147]4#
5# Rick van der Zwet <info@rickvanderzwet.nl>
[9188]6from django.core.cache import cache
[9147]7from django.core.management import setup_environ
[9151]8from django.db.models import Max
[9147]9from django.http import HttpResponse
10from gheat.models import *
11import logging
[9149]12import pygame
[9147]13import sys
[9148]14import tempfile
[9184]15import time
[9147]16
[9150]17# Rending with PIL and computation with numpy has proven to be to slow to be
18# usable, but is still in here for refence purposes.
19try:
20 from PIL import Image
21 import ImageDraw
22 import numpy as np
23except ImportError:
24 pass
25
[9184]26logging.basicConfig(level=logging.DEBUG)
[9148]27log = logging.getLogger('tile')
[9147]28
[9148]29class PyGamePicture():
30 """ Basic PyGame class, allowing simple image manipulations """
31 def __init__(self, method, size):
32 self.surf = pygame.Surface(size,flags=pygame.SRCALPHA)
[9147]33
[9149]34 def center_crop(self,size):
35 """ Resize to make centered rectange from image """
36 new_surf = pygame.Surface(size, flags=pygame.SRCALPHA)
37 curr_size = self.surf.get_size()
38 new_surf.blit(self.surf,(0,0),
39 ((curr_size[0] - size[0]) / 2, (curr_size[1] - size[1]) / 2, size[0], size[1]))
40 self.surf = new_surf
41
[9148]42 def write(self, fh,format='png'):
43 # XXX: How to get a PNG stream directly to the output
44 f = tempfile.NamedTemporaryFile(suffix=format)
45 pygame.image.save(self.surf,f.name)
46 f.seek(0)
47 fh.write(f.read())
[9147]48
[9184]49 def get_image(self,format='png'):
50 f = tempfile.NamedTemporaryFile(suffix=format)
51 pygame.image.save(self.surf,f.name)
52 f.seek(0)
53 return f.read()
[9147]54
[9184]55
[9151]56 def add_circle(self, center, radius, colour=(255,0,0), transparancy=0):
57 """
58 Hack to add lineair gradient circles and merge with the parent. The
59 transparancy can be configured to make the circles to fade out in the
60 beginning
61 """
62 # Make calculations and ranges a whole bunch more easy
63 radius = int(math.ceil(radius))
64
[9149]65 new_surf = pygame.Surface(self.surf.get_size(),flags=pygame.SRCALPHA)
[9151]66 alpha_per_radius = float(2.55 * (100 - transparancy)) / radius
[9148]67 for r in range(radius,1,-1):
[9174]68 alpha = min(255,int((radius - r) * alpha_per_radius))
69 combined_colour = colour + (alpha,)
70 pygame.draw.circle(new_surf,combined_colour,center,r,0)
[9148]71 self.surf.blit(new_surf,(0,0),special_flags=pygame.BLEND_RGBA_MAX)
72
73
74class PILPicture():
75 """ Basic PIL class, allowing simple image manipulations """
[9147]76 im = None
77 def __init__(self, method, size):
78 self.im = Image.new(method, size)
79 self.data = np.array(self.im)
80
[9148]81 def write(self,fh,format='png'):
82 self.im.save(fh,format)
[9147]83
[9148]84 def make_circle(self,draw, center, radius,colour=(0,255,0)):
85 """ Cicle gradient is created by creating smaller and smaller cicles """
86 (center_x, center_y) = center
87 for i in range(0,radius):
88 draw.ellipse(
89 (center_x - radius + i,
90 center_y - radius + i,
91 center_x + radius - i,
92 center_y + radius - i
93 ),
94 colour +(255 * i/(radius * 2),)
95 )
96
[9147]97 def add_circle(self, center, radius, colour):
98 """ Adding a new cicle is a matter of creating a new one in a empty layer
99 and merging it with the current one
100
101 XXX: Very heavy code, should actually only work on the data arrays, instead
102 of doing all the magic with high-level images """
103
104 im_new = Image.new("RGBA", self.im.size)
105 draw = ImageDraw.Draw(im_new)
[9148]106 self.make_circle(draw, center, radius, colour)
[9147]107
108 data2 = np.array(im_new)
109
110 # Add channels to make new images
111 self.data = self.data + data2
112 self.im = Image.fromarray(self.data)
113
114
115
116class LatLonDeg():
117 """ Helper class for coordinate conversions """
118 def __init__(self,lat_deg, lon_deg):
119 self.lat = lat_deg
120 self.lon = lon_deg
121 def __str__(self):
122 return "%.5f,%.5f" % (self.lat, self.lon)
123
124 def deg_per_pixel(self,other,pixel_max):
125 return(LatLonDeg(abs(self.lat - other.lat) / pixel_max, abs(self.lon - other.lon) / pixel_max))
126
127
128
129# Convertions of tile XYZ to WSG coordinates stolen from:
130# http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
131# <stolen>
132import math
133def deg2num(lat_deg, lon_deg, zoom):
134 lat_rad = math.radians(lat_deg)
135 n = 2.0 ** zoom
136 xtile = int((lon_deg + 180.0) / 360.0 * n)
137 ytile = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n)
138 return(xtile, ytile)
139
140def num2deg(xtile, ytile, zoom):
141 n = 2.0 ** zoom
142 lon_deg = xtile / n * 360.0 - 180.0
143 lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
144 lat_deg = math.degrees(lat_rad)
145 return(LatLonDeg(lat_deg,lon_deg))
146# </stolen>
147
148
149def boundbox_deg(x,y,z):
150 """ Calculate the boundingbox for a image """
151 return (num2deg(x,y,z), num2deg(x+1,y+1,z))
152
153
154
[9166]155def make_tile(x,y,z,filter={},colour=(255,0,0)):
[9150]156 """
157 Crude attempt to generate tiles, by placing a gradient circle on a
[9149]158 coordinate point. Generate a larger tile and make sure to plot related
159 points first and then crop it to the required size (250x250).
[9148]160
161 Many stuff NOT implemented yet, like:
[9150]162 - Caching Images.
163 - Conditional Filtering of Meting to allow display of sub-results.
164 - Defining a extra level of transparency if you like to layer multiple tiles
165 on top of each-other.
166 - Color variation, allow the user to dynamically choose a the colour the
167 points to be.
168 - Advanced data plotting, like trying to guess the remainder points.
[9147]169 """
[9150]170
[9149]171 SIZE = 250
172
[9147]173 nw_deg,se_deg = boundbox_deg(x,y,z)
[9149]174
[9147]175
[9148]176 Picture = PyGamePicture
[9149]177 resolution_deg = nw_deg.deg_per_pixel(se_deg, SIZE)
178 # Converting LatLon to Meters is discussed here:
179 # http://stackoverflow.com/questions/3024404/transform-longitude-latitude-into-meters
180 tile_height = float(40008000) / (2 ** z)
181 meters_per_pixel = float(tile_height) / SIZE
[9147]182
[9149]183 # Worst case scenario could a circle with 100% 'outside' our 250x250 range
184 # also add data to the picture as circles are used
185 border_pixels = 100 / meters_per_pixel / 2
186
187 im = Picture("RGBA", (SIZE + border_pixels * 2,) * 2)
188
189 nw_deg.lat += resolution_deg.lat * border_pixels
190 nw_deg.lon -= resolution_deg.lon * border_pixels
191 se_deg.lat -= resolution_deg.lat * border_pixels
192 se_deg.lon += resolution_deg.lon * border_pixels
193
[9147]194 lat_min = 999
195 lon_min = 999
196 lat_max = 0
197 lon_max = 0
[9166]198
199 filter.update({
200 'latitude__lte' : nw_deg.lat,
201 'latitude__gte' : se_deg.lat,
202 'longitude__lte' : se_deg.lon,
203 'longitude__gte' : nw_deg.lon
204 })
[9150]205 # TODO: This is currently hard-coded to display _all_ metingen
[9166]206 metingen = Meting.objects.select_related().filter(**filter)
[9151]207
208 # XXX: Signal is not normalized in the database making it unknown when a
209 # signal is said to be 100% or when it is actually less, currently seems to
210 # copy the raw reported values
211 MAX_SIGNAL = 50
[9152]212 # XXX: The radius relates to the zoom-level we are in, and should represent
213 # a fixed distance, given the scale. Assume signal/distance to be lineair
214 # such that signal 100% = 100m and 1% = 1m.
215 #
216 # XXX: The relation is not lineair but from a more logeritmic scape, as we
217 # are dealing with radio signals
218 #
219 MAX_RANGE = 100
[9147]220
221 def dif(x,y):
[9150]222 """ Return difference between two points """
[9147]223 return max(x,y) - min(x,y)
224
225 for meting in metingen:
226 lat_min = min(lat_min, meting.latitude)
227 lat_max = max(lat_max, meting.latitude)
228 lon_min = min(lon_min, meting.longitude)
229 lon_max = max(lon_max, meting.longitude)
[9172]230 xcoord = int(dif(nw_deg.lon,meting.longitude) / (resolution_deg.lon))
231 ycoord = int(dif(nw_deg.lat,meting.latitude) / (resolution_deg.lat))
[9150]232
[9152]233 # TODO: Please note that this 'logic' technically does apply to WiFi signals,
234 # if you are plotting from the 'source'. When plotting 'measurement' data you
235 # get different patterns and properly need to start looking at techniques like:
236 # Multilateration,Triangulation or Trilateration to recieve 'source' points.
[9148]237 #
[9152]238 # Also you can treat all points as seperate and use techniques like
239 # Multivariate interpolation to make the graphs. A nice overview at:
240 # http://en.wikipedia.org/wiki/Multivariate_interpolation
[9150]241 #
[9152]242 # One very intersting one to look at will be Inverse distance weighting
243 # with examples like this:
244 # http://stackoverflow.com/questions/3104781/inverse-distance-weighted-idw-interpolation-with-python
245 signal_normalized = MAX_RANGE - (MAX_SIGNAL - meting.signaal)
[9166]246 im.add_circle((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - meting.signaal)
[9184]247 #im.add_point((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - meting.signaal)
[9147]248
249 log.info("BoundingBox NW: %s" % nw_deg)
250 log.info("BoundingBox SE: %s" % se_deg)
251 log.info("")
252 log.info("MetingBox NW: %.5f,%.5f" % (lat_max, lon_min))
253 log.info("MetingBox SE: %.5f,%.5f" % (lat_min, lon_max))
254 log.info("")
255 log.info("Metingen Count: %s" % metingen.count())
256
[9149]257 im.center_crop((SIZE,SIZE))
[9147]258 return im
259
260
261# Create your views here.
262def serve_tile(request,zoom,x,y):
[9188]263 """
264 Showcase on caching and tile generation:
265
266 To make it 'easy' we use two types of caching here, memory cache expiring
267 at 30 seconds and database cache, always persistent storage on database.
268 """
269
[9184]270 hash_key = TileCache.make_key("%s %s %s %s" % (zoom, x ,y, request.GET.urlencode()))
[9188]271 data = cache.get(hash_key)
[9184]272
[9188]273 if not data:
274 try:
275 d = TileCache.objects.get(key=hash_key)
276 data = d.data
277 cache.set(hash_key,data, 30)
278 except TileCache.DoesNotExist:
279 #log.setLevel(logging.DEBUG)
280 filter = {}
281 colour = (255,0,0)
282 for key, value in request.GET.iteritems():
283 if key == 'colour':
284 colour = tuple(map(int,value.split(',')))
285 else:
286 filter[key] = value
287 now = time.time()
288 im = make_tile(int(x),int(y),int(zoom),filter=filter,colour=colour)
289 log.info("Processing time: %s" % (time.time() - now))
290 data = im.get_image('png')
291 cache.set(hash_key,data, 30)
292 r = TileCache.objects.create(key=hash_key,data=data)
293
[9147]294 response = HttpResponse(mimetype="image/png")
[9184]295 response.write(data)
[9147]296 return response
[9184]297
Note: See TracBrowser for help on using the repository browser.