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

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