| 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>
|
|---|
| 6 | from collections import defaultdict
|
|---|
| 7 | from django.core.management import setup_environ
|
|---|
| 8 | from django.db.models import Max
|
|---|
| 9 | from django.http import HttpResponse
|
|---|
| 10 | from django.views.decorators.cache import cache_page
|
|---|
| 11 | from gheat.models import *
|
|---|
| 12 | import os
|
|---|
| 13 | import pygame
|
|---|
| 14 | import sys
|
|---|
| 15 | import tempfile
|
|---|
| 16 | import time
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 | class PyGamePicture():
|
|---|
| 20 | """ Basic PyGame class, allowing simple image manipulations """
|
|---|
| 21 | def __init__(self, method, size):
|
|---|
| 22 | self.surf = pygame.Surface(size,flags=pygame.SRCALPHA)
|
|---|
| 23 |
|
|---|
| 24 | def center_crop(self,size):
|
|---|
| 25 | """ Resize to make centered rectange from image """
|
|---|
| 26 | new_surf = pygame.Surface(size, flags=pygame.SRCALPHA)
|
|---|
| 27 | curr_size = self.surf.get_size()
|
|---|
| 28 | new_surf.blit(self.surf,(0,0),
|
|---|
| 29 | ((curr_size[0] - size[0]) / 2, (curr_size[1] - size[1]) / 2, size[0], size[1]))
|
|---|
| 30 | self.surf = new_surf
|
|---|
| 31 |
|
|---|
| 32 | def save_and_get_image(self,filename):
|
|---|
| 33 | """ Save the file to the location and return the file """
|
|---|
| 34 | basedir = os.path.dirname(filename)
|
|---|
| 35 | if not os.path.isdir(basedir):
|
|---|
| 36 | os.makedirs(basedir)
|
|---|
| 37 | pygame.image.save(self.surf,filename)
|
|---|
| 38 | return open(filename,'r').read()
|
|---|
| 39 |
|
|---|
| 40 | def get_image(self,format='png'):
|
|---|
| 41 | f = tempfile.NamedTemporaryFile(suffix=format)
|
|---|
| 42 | pygame.image.save(self.surf,f.name)
|
|---|
| 43 | f.seek(0)
|
|---|
| 44 | return f.read()
|
|---|
| 45 |
|
|---|
| 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 |
|
|---|
| 66 | class LatLonDeg():
|
|---|
| 67 | """ Helper class for coordinate conversions """
|
|---|
| 68 | def __init__(self,lat_deg, lon_deg):
|
|---|
| 69 | self.lat = lat_deg
|
|---|
| 70 | self.lon = lon_deg
|
|---|
| 71 | def __str__(self):
|
|---|
| 72 | return "%.5f,%.5f" % (self.lat, self.lon)
|
|---|
| 73 |
|
|---|
| 74 | def deg_per_pixel(self,other,pixel_max):
|
|---|
| 75 | return(LatLonDeg(abs(self.lat - other.lat) / pixel_max, abs(self.lon - other.lon) / pixel_max))
|
|---|
| 76 |
|
|---|
| 77 |
|
|---|
| 78 |
|
|---|
| 79 | # Convertions of tile XYZ to WSG coordinates stolen from:
|
|---|
| 80 | # http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
|
|---|
| 81 | # <stolen>
|
|---|
| 82 | import math
|
|---|
| 83 | def deg2num(lat_deg, lon_deg, zoom):
|
|---|
| 84 | lat_rad = math.radians(lat_deg)
|
|---|
| 85 | n = 2.0 ** zoom
|
|---|
| 86 | xtile = int((lon_deg + 180.0) / 360.0 * n)
|
|---|
| 87 | ytile = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n)
|
|---|
| 88 | return(xtile, ytile)
|
|---|
| 89 |
|
|---|
| 90 | def num2deg(xtile, ytile, zoom):
|
|---|
| 91 | n = 2.0 ** zoom
|
|---|
| 92 | lon_deg = xtile / n * 360.0 - 180.0
|
|---|
| 93 | lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
|
|---|
| 94 | lat_deg = math.degrees(lat_rad)
|
|---|
| 95 | return(LatLonDeg(lat_deg,lon_deg))
|
|---|
| 96 | # </stolen>
|
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 | def boundbox_deg(x,y,z):
|
|---|
| 100 | """ Calculate the boundingbox for a image """
|
|---|
| 101 | return (num2deg(x,y,z), num2deg(x+1,y+1,z))
|
|---|
| 102 |
|
|---|
| 103 |
|
|---|
| 104 |
|
|---|
| 105 | def make_tile(x,y,z,filter={},colour=(255,0,0)):
|
|---|
| 106 | """
|
|---|
| 107 | Crude attempt to generate tiles, by placing a gradient circle on a
|
|---|
| 108 | coordinate point. Generate a larger tile and make sure to plot related
|
|---|
| 109 | points first and then crop it to the required size (250x250).
|
|---|
| 110 |
|
|---|
| 111 | Many stuff NOT implemented yet, like:
|
|---|
| 112 | - Caching Images.
|
|---|
| 113 | - Conditional Filtering of Meting to allow display of sub-results.
|
|---|
| 114 | - Defining a extra level of transparency if you like to layer multiple tiles
|
|---|
| 115 | on top of each-other.
|
|---|
| 116 | - Color variation, allow the user to dynamically choose a the colour the
|
|---|
| 117 | points to be.
|
|---|
| 118 | - Advanced data plotting, like trying to guess the remainder points.
|
|---|
| 119 | """
|
|---|
| 120 |
|
|---|
| 121 | SIZE = 250
|
|---|
| 122 |
|
|---|
| 123 | nw_deg,se_deg = boundbox_deg(x,y,z)
|
|---|
| 124 |
|
|---|
| 125 |
|
|---|
| 126 | Picture = PyGamePicture
|
|---|
| 127 | resolution_deg = nw_deg.deg_per_pixel(se_deg, SIZE)
|
|---|
| 128 | # Converting LatLon to Meters is discussed here:
|
|---|
| 129 | # http://stackoverflow.com/questions/3024404/transform-longitude-latitude-into-meters
|
|---|
| 130 | tile_height = float(40008000) / (2 ** z)
|
|---|
| 131 | meters_per_pixel = float(tile_height) / SIZE
|
|---|
| 132 |
|
|---|
| 133 | # Worst case scenario could a circle with 100% 'outside' our 250x250 range
|
|---|
| 134 | # also add data to the picture as circles are used
|
|---|
| 135 | border_pixels = 100 / meters_per_pixel / 2
|
|---|
| 136 |
|
|---|
| 137 | im = Picture("RGBA", (SIZE + border_pixels * 2,) * 2)
|
|---|
| 138 |
|
|---|
| 139 | nw_deg.lat += resolution_deg.lat * border_pixels
|
|---|
| 140 | nw_deg.lon -= resolution_deg.lon * border_pixels
|
|---|
| 141 | se_deg.lat -= resolution_deg.lat * border_pixels
|
|---|
| 142 | se_deg.lon += resolution_deg.lon * border_pixels
|
|---|
| 143 |
|
|---|
| 144 | lat_min = 999
|
|---|
| 145 | lon_min = 999
|
|---|
| 146 | lat_max = 0
|
|---|
| 147 | lon_max = 0
|
|---|
| 148 |
|
|---|
| 149 | for key in filter.keys():
|
|---|
| 150 | if filter[key] == 'all':
|
|---|
| 151 | del filter[key]
|
|---|
| 152 |
|
|---|
| 153 | filter.update({
|
|---|
| 154 | 'latitude__lte' : nw_deg.lat,
|
|---|
| 155 | 'latitude__gte' : se_deg.lat,
|
|---|
| 156 | 'longitude__lte' : se_deg.lon,
|
|---|
| 157 | 'longitude__gte' : nw_deg.lon
|
|---|
| 158 | })
|
|---|
| 159 | # Limit such that high level zooms does not get the whole database
|
|---|
| 160 | metingen = Meting.objects.filter(**filter).order_by('?')[:1000].values_list('latitude', 'longitude', 'signaal')
|
|---|
| 161 |
|
|---|
| 162 | # Round numbers example to a less fine grain measurements
|
|---|
| 163 | # d = defaultdict(list)
|
|---|
| 164 | # for lat,lon, signaal in metingen:
|
|---|
| 165 | # d[(round(lat,5),round(lon,5))].append(signaal)
|
|---|
| 166 |
|
|---|
| 167 | # metingen = []
|
|---|
| 168 | # for (lat,lon),signals in d.iteritems():
|
|---|
| 169 | # metingen.append((lat,lon,max(signals)))
|
|---|
| 170 |
|
|---|
| 171 | # XXX: Signal is not normalized in the database making it unknown when a
|
|---|
| 172 | # signal is said to be 100% or when it is actually less, currently seems to
|
|---|
| 173 | # copy the raw reported values
|
|---|
| 174 | MAX_SIGNAL = 50
|
|---|
| 175 | # XXX: The radius relates to the zoom-level we are in, and should represent
|
|---|
| 176 | # a fixed distance, given the scale. Assume signal/distance to be lineair
|
|---|
| 177 | # such that signal 100% = 100m and 1% = 1m.
|
|---|
| 178 | #
|
|---|
| 179 | # XXX: The relation is not lineair but from a more logeritmic scape, as we
|
|---|
| 180 | # are dealing with radio signals
|
|---|
| 181 | #
|
|---|
| 182 | MAX_RANGE = 100
|
|---|
| 183 |
|
|---|
| 184 | def dif(x,y):
|
|---|
| 185 | """ Return difference between two points """
|
|---|
| 186 | return max(x,y) - min(x,y)
|
|---|
| 187 |
|
|---|
| 188 | for (latitude, longitude, signaal) in metingen:
|
|---|
| 189 | lat_min = min(lat_min, latitude)
|
|---|
| 190 | lat_max = max(lat_max, latitude)
|
|---|
| 191 | lon_min = min(lon_min, longitude)
|
|---|
| 192 | lon_max = max(lon_max, longitude)
|
|---|
| 193 | xcoord = int(dif(nw_deg.lon,longitude) / (resolution_deg.lon))
|
|---|
| 194 | ycoord = int(dif(nw_deg.lat,latitude) / (resolution_deg.lat))
|
|---|
| 195 |
|
|---|
| 196 | # TODO: Please note that this 'logic' technically does apply to WiFi signals,
|
|---|
| 197 | # if you are plotting from the 'source'. When plotting 'measurement' data you
|
|---|
| 198 | # get different patterns and properly need to start looking at techniques like:
|
|---|
| 199 | # Multilateration,Triangulation or Trilateration to recieve 'source' points.
|
|---|
| 200 | #
|
|---|
| 201 | # Also you can treat all points as seperate and use techniques like
|
|---|
| 202 | # Multivariate interpolation to make the graphs. A nice overview at:
|
|---|
| 203 | # http://en.wikipedia.org/wiki/Multivariate_interpolation
|
|---|
| 204 | #
|
|---|
| 205 | # One very intersting one to look at will be Inverse distance weighting
|
|---|
| 206 | # with examples like this:
|
|---|
| 207 | # http://stackoverflow.com/questions/3104781/inverse-distance-weighted-idw-interpolation-with-python
|
|---|
| 208 | signal_normalized = MAX_RANGE - (MAX_SIGNAL - signaal)
|
|---|
| 209 | im.add_circle((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - signaal)
|
|---|
| 210 | #im.add_point((xcoord,ycoord),float(signal_normalized) / meters_per_pixel,colour, MAX_SIGNAL - meting.signaal)
|
|---|
| 211 |
|
|---|
| 212 | im.center_crop((SIZE,SIZE))
|
|---|
| 213 | return im
|
|---|
| 214 |
|
|---|
| 215 | def pre_process_tile(request,zoom,x,y):
|
|---|
| 216 | filter = {}
|
|---|
| 217 | colour = (255,0,0)
|
|---|
| 218 | for key, value in request.GET.iteritems():
|
|---|
| 219 | if key == 'colour':
|
|---|
| 220 | colour = tuple(map(int,value.split(',')))
|
|---|
| 221 | else:
|
|---|
| 222 | filter[key] = value
|
|---|
| 223 | now = time.time()
|
|---|
| 224 | im = make_tile(int(x),int(y),int(zoom),filter=filter,colour=colour)
|
|---|
| 225 | return im
|
|---|
| 226 |
|
|---|
| 227 | # Create your views here.
|
|---|
| 228 | # N.B: This cache is handly is you are using in standalone mode
|
|---|
| 229 | #@cache_page(60 * 60 * 24, cache="tile_cache")
|
|---|
| 230 | def serve_tile(request,zoom,x,y):
|
|---|
| 231 | im = pre_process_tile(request,zoom,x,y)
|
|---|
| 232 | data = im.get_image('png')
|
|---|
| 233 | return HttpResponse(data,mimetype="image/png")
|
|---|
| 234 |
|
|---|
| 235 | def fixed_wl_only(request,zoom,x,y):
|
|---|
| 236 | """ Pre-render and save attempt """
|
|---|
| 237 | im = pre_process_tile(request,zoom,x,y)
|
|---|
| 238 | data = im.save_and_get_image('/usr/local/var/django/tile/fixed/wl-only/%s/%s,%s.png' % (zoom, x, y))
|
|---|
| 239 | return HttpResponse(data,mimetype="image/png")
|
|---|