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

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

Allow selecting all entries.

  • Property svn:executable set to *
File size: 9.5 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
[9577]198 for key in filter.keys():
199 if filter[key] == 'all':
200 del filter[key]
201
[9166]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 })
[9549]208 # Limit such that high level zooms does not get the whole database
[9571]209 metingen = Meting.objects.filter(**filter).order_by('?')[:1000].values_list('latitude', 'longitude', 'signaal')
[9151]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
[9152]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
[9147]223
224 def dif(x,y):
[9150]225 """ Return difference between two points """
[9147]226 return max(x,y) - min(x,y)
227
[9549]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))
[9150]235
[9152]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.
[9148]240 #
[9152]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
[9150]244 #
[9152]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
[9549]248 signal_normalized = MAX_RANGE - (MAX_SIGNAL - signaal)
249 im.add_circle((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - signaal)
[9184]250 #im.add_point((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - meting.signaal)
[9147]251
[9149]252 im.center_crop((SIZE,SIZE))
[9147]253 return im
254
[9549]255def pre_process_tile(request,zoom,x,y):
[9392]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)
[9549]265 return im
266
267# Create your views here.
[9572]268# N.B: This cache is handly is you are using in standalone mode
269#@cache_page(60 * 60 * 24, cache="tile_cache")
[9549]270def serve_tile(request,zoom,x,y):
271 im = pre_process_tile(request,zoom,x,y)
[9392]272 data = im.get_image('png')
[9549]273 return HttpResponse(data,mimetype="image/png")
[9188]274
[9549]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.