source: src/django_gheat/website/tile.py@ 9182

Last change on this file since 9182 was 9174, checked in by rick, 14 years ago

Rounding errors, colour cannot be more than 255

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