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

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

Signal is not normalized so make it so. Also the starting transparancy should
be related to the initial entry field.

While we are here, explaining in-depth the issues we are facing when plotting
the data.

  • Property svn:executable set to *
File size: 8.6 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 = (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):
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 # TODO: This is currently hard-coded to display _all_ metingen
191 metingen = Meting.objects.select_related().filter(
192 latitude__lte=nw_deg.lat,latitude__gte=se_deg.lat,
193 longitude__lte=se_deg.lon,longitude__gte=nw_deg.lon)
194
195 # XXX: Signal is not normalized in the database making it unknown when a
196 # signal is said to be 100% or when it is actually less, currently seems to
197 # copy the raw reported values
198 MAX_SIGNAL = 50
199 # XXX: The radius relates to the zoom-level we are in, and should represent
200 # a fixed distance, given the scale. Assume signal/distance to be lineair
201 # such that signal 100% = 100m and 1% = 1m.
202 #
203 # XXX: The relation is not lineair but from a more logeritmic scape, as we
204 # are dealing with radio signals
205 #
206 MAX_RANGE = 100
207
208 def dif(x,y):
209 """ Return difference between two points """
210 return max(x,y) - min(x,y)
211
212 for meting in metingen:
213 lat_min = min(lat_min, meting.latitude)
214 lat_max = max(lat_max, meting.latitude)
215 lon_min = min(lon_min, meting.longitude)
216 lon_max = max(lon_max, meting.longitude)
217 xcoord = dif(nw_deg.lon,meting.longitude) / (resolution_deg.lon)
218 ycoord = dif(nw_deg.lat,meting.latitude) / (resolution_deg.lat)
219 log.info(meting.accespoint.ssid, meting.latitude, meting.longitude, xcoord, ycoord)
220
221 # TODO: Please note that this 'logic' technically does apply to WiFi signals,
222 # if you are plotting from the 'source'. When plotting 'measurement' data you
223 # get different patterns and properly need to start looking at techniques like:
224 # Multilateration,Triangulation or Trilateration to recieve 'source' points.
225 #
226 # Also you can treat all points as seperate and use techniques like
227 # Multivariate interpolation to make the graphs. A nice overview at:
228 # http://en.wikipedia.org/wiki/Multivariate_interpolation
229 #
230 # One very intersting one to look at will be Inverse distance weighting
231 # with examples like this:
232 # http://stackoverflow.com/questions/3104781/inverse-distance-weighted-idw-interpolation-with-python
233 signal_normalized = MAX_RANGE - (MAX_SIGNAL - meting.signaal)
234 im.add_circle((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,(255,0,0), MAX_SIGNAL - meting.signaal)
235
236 log.info("BoundingBox NW: %s" % nw_deg)
237 log.info("BoundingBox SE: %s" % se_deg)
238 log.info("")
239 log.info("MetingBox NW: %.5f,%.5f" % (lat_max, lon_min))
240 log.info("MetingBox SE: %.5f,%.5f" % (lat_min, lon_max))
241 log.info("")
242 log.info("Metingen Count: %s" % metingen.count())
243
244 im.center_crop((SIZE,SIZE))
245 return im
246
247
248# Create your views here.
249def serve_tile(request,zoom,x,y):
250 im = make_tile(int(x),int(y),int(zoom))
251 response = HttpResponse(mimetype="image/png")
252 im.write(response,'png')
253 return response
Note: See TracBrowser for help on using the repository browser.