[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>
|
---|
[9188] | 6 | from django.core.cache import cache
|
---|
[9147] | 7 | from django.core.management import setup_environ
|
---|
[9151] | 8 | from django.db.models import Max
|
---|
[9147] | 9 | from django.http import HttpResponse
|
---|
| 10 | from gheat.models import *
|
---|
| 11 | import logging
|
---|
[9149] | 12 | import pygame
|
---|
[9147] | 13 | import sys
|
---|
[9148] | 14 | import tempfile
|
---|
[9184] | 15 | import time
|
---|
[9147] | 16 |
|
---|
[9150] | 17 | # Rending with PIL and computation with numpy has proven to be to slow to be
|
---|
| 18 | # usable, but is still in here for refence purposes.
|
---|
| 19 | try:
|
---|
| 20 | from PIL import Image
|
---|
| 21 | import ImageDraw
|
---|
| 22 | import numpy as np
|
---|
| 23 | except ImportError:
|
---|
| 24 | pass
|
---|
| 25 |
|
---|
[9184] | 26 | logging.basicConfig(level=logging.DEBUG)
|
---|
[9148] | 27 | log = logging.getLogger('tile')
|
---|
[9147] | 28 |
|
---|
[9148] | 29 | class PyGamePicture():
|
---|
| 30 | """ Basic PyGame class, allowing simple image manipulations """
|
---|
| 31 | def __init__(self, method, size):
|
---|
| 32 | self.surf = pygame.Surface(size,flags=pygame.SRCALPHA)
|
---|
[9147] | 33 |
|
---|
[9149] | 34 | def center_crop(self,size):
|
---|
| 35 | """ Resize to make centered rectange from image """
|
---|
| 36 | new_surf = pygame.Surface(size, flags=pygame.SRCALPHA)
|
---|
| 37 | curr_size = self.surf.get_size()
|
---|
| 38 | new_surf.blit(self.surf,(0,0),
|
---|
| 39 | ((curr_size[0] - size[0]) / 2, (curr_size[1] - size[1]) / 2, size[0], size[1]))
|
---|
| 40 | self.surf = new_surf
|
---|
| 41 |
|
---|
[9148] | 42 | def write(self, fh,format='png'):
|
---|
| 43 | # XXX: How to get a PNG stream directly to the output
|
---|
| 44 | f = tempfile.NamedTemporaryFile(suffix=format)
|
---|
| 45 | pygame.image.save(self.surf,f.name)
|
---|
| 46 | f.seek(0)
|
---|
| 47 | fh.write(f.read())
|
---|
[9147] | 48 |
|
---|
[9184] | 49 | def get_image(self,format='png'):
|
---|
| 50 | f = tempfile.NamedTemporaryFile(suffix=format)
|
---|
| 51 | pygame.image.save(self.surf,f.name)
|
---|
| 52 | f.seek(0)
|
---|
| 53 | return f.read()
|
---|
[9147] | 54 |
|
---|
[9184] | 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 |
|
---|
| 74 | class 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 |
|
---|
| 116 | class 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>
|
---|
| 132 | import math
|
---|
| 133 | def 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 |
|
---|
| 140 | def 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 |
|
---|
| 149 | def 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] | 155 | def 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 |
|
---|
| 199 | filter.update({
|
---|
| 200 | 'latitude__lte' : nw_deg.lat,
|
---|
| 201 | 'latitude__gte' : se_deg.lat,
|
---|
| 202 | 'longitude__lte' : se_deg.lon,
|
---|
| 203 | 'longitude__gte' : nw_deg.lon
|
---|
| 204 | })
|
---|
[9150] | 205 | # TODO: This is currently hard-coded to display _all_ metingen
|
---|
[9166] | 206 | metingen = Meting.objects.select_related().filter(**filter)
|
---|
[9151] | 207 |
|
---|
| 208 | # XXX: Signal is not normalized in the database making it unknown when a
|
---|
| 209 | # signal is said to be 100% or when it is actually less, currently seems to
|
---|
| 210 | # copy the raw reported values
|
---|
| 211 | MAX_SIGNAL = 50
|
---|
[9152] | 212 | # XXX: The radius relates to the zoom-level we are in, and should represent
|
---|
| 213 | # a fixed distance, given the scale. Assume signal/distance to be lineair
|
---|
| 214 | # such that signal 100% = 100m and 1% = 1m.
|
---|
| 215 | #
|
---|
| 216 | # XXX: The relation is not lineair but from a more logeritmic scape, as we
|
---|
| 217 | # are dealing with radio signals
|
---|
| 218 | #
|
---|
| 219 | MAX_RANGE = 100
|
---|
[9147] | 220 |
|
---|
| 221 | def dif(x,y):
|
---|
[9150] | 222 | """ Return difference between two points """
|
---|
[9147] | 223 | return max(x,y) - min(x,y)
|
---|
| 224 |
|
---|
| 225 | for meting in metingen:
|
---|
| 226 | lat_min = min(lat_min, meting.latitude)
|
---|
| 227 | lat_max = max(lat_max, meting.latitude)
|
---|
| 228 | lon_min = min(lon_min, meting.longitude)
|
---|
| 229 | lon_max = max(lon_max, meting.longitude)
|
---|
[9172] | 230 | xcoord = int(dif(nw_deg.lon,meting.longitude) / (resolution_deg.lon))
|
---|
| 231 | ycoord = int(dif(nw_deg.lat,meting.latitude) / (resolution_deg.lat))
|
---|
[9150] | 232 |
|
---|
[9152] | 233 | # TODO: Please note that this 'logic' technically does apply to WiFi signals,
|
---|
| 234 | # if you are plotting from the 'source'. When plotting 'measurement' data you
|
---|
| 235 | # get different patterns and properly need to start looking at techniques like:
|
---|
| 236 | # Multilateration,Triangulation or Trilateration to recieve 'source' points.
|
---|
[9148] | 237 | #
|
---|
[9152] | 238 | # Also you can treat all points as seperate and use techniques like
|
---|
| 239 | # Multivariate interpolation to make the graphs. A nice overview at:
|
---|
| 240 | # http://en.wikipedia.org/wiki/Multivariate_interpolation
|
---|
[9150] | 241 | #
|
---|
[9152] | 242 | # One very intersting one to look at will be Inverse distance weighting
|
---|
| 243 | # with examples like this:
|
---|
| 244 | # http://stackoverflow.com/questions/3104781/inverse-distance-weighted-idw-interpolation-with-python
|
---|
| 245 | signal_normalized = MAX_RANGE - (MAX_SIGNAL - meting.signaal)
|
---|
[9166] | 246 | im.add_circle((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - meting.signaal)
|
---|
[9184] | 247 | #im.add_point((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - meting.signaal)
|
---|
[9147] | 248 |
|
---|
| 249 | log.info("BoundingBox NW: %s" % nw_deg)
|
---|
| 250 | log.info("BoundingBox SE: %s" % se_deg)
|
---|
| 251 | log.info("")
|
---|
| 252 | log.info("MetingBox NW: %.5f,%.5f" % (lat_max, lon_min))
|
---|
| 253 | log.info("MetingBox SE: %.5f,%.5f" % (lat_min, lon_max))
|
---|
| 254 | log.info("")
|
---|
| 255 | log.info("Metingen Count: %s" % metingen.count())
|
---|
| 256 |
|
---|
[9149] | 257 | im.center_crop((SIZE,SIZE))
|
---|
[9147] | 258 | return im
|
---|
| 259 |
|
---|
| 260 |
|
---|
| 261 | # Create your views here.
|
---|
| 262 | def serve_tile(request,zoom,x,y):
|
---|
[9188] | 263 | """
|
---|
| 264 | Showcase on caching and tile generation:
|
---|
| 265 |
|
---|
| 266 | To make it 'easy' we use two types of caching here, memory cache expiring
|
---|
| 267 | at 30 seconds and database cache, always persistent storage on database.
|
---|
| 268 | """
|
---|
| 269 |
|
---|
[9184] | 270 | hash_key = TileCache.make_key("%s %s %s %s" % (zoom, x ,y, request.GET.urlencode()))
|
---|
[9188] | 271 | data = cache.get(hash_key)
|
---|
[9184] | 272 |
|
---|
[9188] | 273 | if not data:
|
---|
| 274 | try:
|
---|
| 275 | d = TileCache.objects.get(key=hash_key)
|
---|
| 276 | data = d.data
|
---|
| 277 | cache.set(hash_key,data, 30)
|
---|
| 278 | except TileCache.DoesNotExist:
|
---|
| 279 | #log.setLevel(logging.DEBUG)
|
---|
| 280 | filter = {}
|
---|
| 281 | colour = (255,0,0)
|
---|
| 282 | for key, value in request.GET.iteritems():
|
---|
| 283 | if key == 'colour':
|
---|
| 284 | colour = tuple(map(int,value.split(',')))
|
---|
| 285 | else:
|
---|
| 286 | filter[key] = value
|
---|
| 287 | now = time.time()
|
---|
| 288 | im = make_tile(int(x),int(y),int(zoom),filter=filter,colour=colour)
|
---|
| 289 | log.info("Processing time: %s" % (time.time() - now))
|
---|
| 290 | data = im.get_image('png')
|
---|
| 291 | cache.set(hash_key,data, 30)
|
---|
| 292 | r = TileCache.objects.create(key=hash_key,data=data)
|
---|
| 293 |
|
---|
[9147] | 294 | response = HttpResponse(mimetype="image/png")
|
---|
[9184] | 295 | response.write(data)
|
---|
[9147] | 296 | return response
|
---|
[9184] | 297 |
|
---|