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

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

Example to reduce the fine grain results if needed.

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