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

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

Conditional tranparenty, to make sure points with the same value have the same colour.

  • Property svn:executable set to *
File size: 8.0 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
200 def dif(x,y):
201 """ Return difference between two points """
202 return max(x,y) - min(x,y)
203
204 for meting in metingen:
205 lat_min = min(lat_min, meting.latitude)
206 lat_max = max(lat_max, meting.latitude)
207 lon_min = min(lon_min, meting.longitude)
208 lon_max = max(lon_max, meting.longitude)
209 xcoord = dif(nw_deg.lon,meting.longitude) / (resolution_deg.lon)
210 ycoord = dif(nw_deg.lat,meting.latitude) / (resolution_deg.lat)
211 log.info(meting.accespoint.ssid, meting.latitude, meting.longitude, xcoord, ycoord)
212
213 # XXX: The radius relates to the zoom-level we are in, and should represent
214 # a fixed distance, given the scale. Assume signal/distance to be lineair
215 # such that signal 100% = 100m and 1% = 1m.
216 #
217 # XXX: The relation is not lineair but from a more logeritmic scape, as we
218 # are dealing with radio signals
219 #
220 # TODO: Please note that this 'logic' technically does any apply to WiFi signals,
221 # if you are plotting from the 'source'. With measured data you get
222 # different patterns.
223 #
224 im.add_circle((xcoord,ycoord),float(meting.signaal) / meters_per_pixel,(255,0,0),MAX_SIGNAL - meting.signaal)
225
226 log.info("BoundingBox NW: %s" % nw_deg)
227 log.info("BoundingBox SE: %s" % se_deg)
228 log.info("")
229 log.info("MetingBox NW: %.5f,%.5f" % (lat_max, lon_min))
230 log.info("MetingBox SE: %.5f,%.5f" % (lat_min, lon_max))
231 log.info("")
232 log.info("Metingen Count: %s" % metingen.count())
233
234 im.center_crop((SIZE,SIZE))
235 return im
236
237
238# Create your views here.
239def serve_tile(request,zoom,x,y):
240 im = make_tile(int(x),int(y),int(zoom))
241 response = HttpResponse(mimetype="image/png")
242 im.write(response,'png')
243 return response
Note: See TracBrowser for help on using the repository browser.