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

Last change on this file since 9569 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
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>
6from django.core.management import setup_environ
[9151]7from django.db.models import Max
[9147]8from django.http import HttpResponse
[9392]9from django.views.decorators.cache import cache_page
[9147]10from gheat.models import *
[9549]11import os
[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
[9148]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)
[9147]30
[9149]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
[9549]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()
[9147]46
[9184]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()
[9147]52
[9184]53
[9549]54
[9151]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
[9149]64 new_surf = pygame.Surface(self.surf.get_size(),flags=pygame.SRCALPHA)
[9151]65 alpha_per_radius = float(2.55 * (100 - transparancy)) / radius
[9148]66 for r in range(radius,1,-1):
[9174]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)
[9148]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 """
[9147]75 im = None
76 def __init__(self, method, size):
77 self.im = Image.new(method, size)
78 self.data = np.array(self.im)
79
[9148]80 def write(self,fh,format='png'):
81 self.im.save(fh,format)
[9147]82
[9148]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
[9147]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)
[9148]105 self.make_circle(draw, center, radius, colour)
[9147]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
[9166]154def make_tile(x,y,z,filter={},colour=(255,0,0)):
[9150]155 """
156 Crude attempt to generate tiles, by placing a gradient circle on a
[9149]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).
[9148]159
160 Many stuff NOT implemented yet, like:
[9150]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.
[9147]168 """
[9150]169
[9149]170 SIZE = 250
171
[9147]172 nw_deg,se_deg = boundbox_deg(x,y,z)
[9149]173
[9147]174
[9148]175 Picture = PyGamePicture
[9149]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
[9147]181
[9149]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
[9147]193 lat_min = 999
194 lon_min = 999
195 lat_max = 0
196 lon_max = 0
[9166]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 })
[9549]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')
[9151]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
[9152]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
[9147]219
220 def dif(x,y):
[9150]221 """ Return difference between two points """
[9147]222 return max(x,y) - min(x,y)
223
[9549]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))
[9150]231
[9152]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.
[9148]236 #
[9152]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
[9150]240 #
[9152]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
[9549]244 signal_normalized = MAX_RANGE - (MAX_SIGNAL - signaal)
245 im.add_circle((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - signaal)
[9184]246 #im.add_point((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - meting.signaal)
[9147]247
[9149]248 im.center_crop((SIZE,SIZE))
[9147]249 return im
250
[9549]251def pre_process_tile(request,zoom,x,y):
[9392]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)
[9549]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)
[9392]267 data = im.get_image('png')
[9549]268 return HttpResponse(data,mimetype="image/png")
[9188]269
[9549]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.