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

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

Allow selecting all entries.

  • Property svn:executable set to *
File size: 9.5 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 for key in filter.keys():
199 if filter[key] == 'all':
200 del filter[key]
201
202 filter.update({
203 'latitude__lte' : nw_deg.lat,
204 'latitude__gte' : se_deg.lat,
205 'longitude__lte' : se_deg.lon,
206 'longitude__gte' : nw_deg.lon
207 })
208 # Limit such that high level zooms does not get the whole database
209 metingen = Meting.objects.filter(**filter).order_by('?')[:1000].values_list('latitude', 'longitude', 'signaal')
210
211 # XXX: Signal is not normalized in the database making it unknown when a
212 # signal is said to be 100% or when it is actually less, currently seems to
213 # copy the raw reported values
214 MAX_SIGNAL = 50
215 # XXX: The radius relates to the zoom-level we are in, and should represent
216 # a fixed distance, given the scale. Assume signal/distance to be lineair
217 # such that signal 100% = 100m and 1% = 1m.
218 #
219 # XXX: The relation is not lineair but from a more logeritmic scape, as we
220 # are dealing with radio signals
221 #
222 MAX_RANGE = 100
223
224 def dif(x,y):
225 """ Return difference between two points """
226 return max(x,y) - min(x,y)
227
228 for (latitude, longitude, signaal) in metingen:
229 lat_min = min(lat_min, latitude)
230 lat_max = max(lat_max, latitude)
231 lon_min = min(lon_min, longitude)
232 lon_max = max(lon_max, longitude)
233 xcoord = int(dif(nw_deg.lon,longitude) / (resolution_deg.lon))
234 ycoord = int(dif(nw_deg.lat,latitude) / (resolution_deg.lat))
235
236 # TODO: Please note that this 'logic' technically does apply to WiFi signals,
237 # if you are plotting from the 'source'. When plotting 'measurement' data you
238 # get different patterns and properly need to start looking at techniques like:
239 # Multilateration,Triangulation or Trilateration to recieve 'source' points.
240 #
241 # Also you can treat all points as seperate and use techniques like
242 # Multivariate interpolation to make the graphs. A nice overview at:
243 # http://en.wikipedia.org/wiki/Multivariate_interpolation
244 #
245 # One very intersting one to look at will be Inverse distance weighting
246 # with examples like this:
247 # http://stackoverflow.com/questions/3104781/inverse-distance-weighted-idw-interpolation-with-python
248 signal_normalized = MAX_RANGE - (MAX_SIGNAL - signaal)
249 im.add_circle((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - signaal)
250 #im.add_point((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - meting.signaal)
251
252 im.center_crop((SIZE,SIZE))
253 return im
254
255def pre_process_tile(request,zoom,x,y):
256 filter = {}
257 colour = (255,0,0)
258 for key, value in request.GET.iteritems():
259 if key == 'colour':
260 colour = tuple(map(int,value.split(',')))
261 else:
262 filter[key] = value
263 now = time.time()
264 im = make_tile(int(x),int(y),int(zoom),filter=filter,colour=colour)
265 return im
266
267# Create your views here.
268# N.B: This cache is handly is you are using in standalone mode
269#@cache_page(60 * 60 * 24, cache="tile_cache")
270def serve_tile(request,zoom,x,y):
271 im = pre_process_tile(request,zoom,x,y)
272 data = im.get_image('png')
273 return HttpResponse(data,mimetype="image/png")
274
275def fixed_wl_only(request,zoom,x,y):
276 """ Pre-render and save attempt """
277 im = pre_process_tile(request,zoom,x,y)
278 data = im.save_and_get_image('/usr/local/var/django/tile/fixed/wl-only/%s/%s,%s.png' % (zoom, x, y))
279 return HttpResponse(data,mimetype="image/png")
Note: See TracBrowser for help on using the repository browser.