source: src/django_gheat/wlheatmap/tile.py@ 9546

Last change on this file since 9546 was 9398, checked in by rick, 13 years ago

Giving the cache a special key, allows the cache to be cleared more easy.

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