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

Last change on this file since 9172 was 9172, checked in by dennisw, 14 years ago

Changed xcoord & ycoord to INT. Works now, and no noticible differences in tiles as far as I can see.

Playing with nodelist.

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