diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..fbae63d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +branch=True +include=*heatmap* diff --git a/.gitignore b/.gitignore index a4c6eec..23fc65f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ pip-log.txt .coverage .tox nosetests.xml +/htmlcov # Translations *.mo @@ -44,4 +45,4 @@ nosetests.xml .DS_Store *.png -*.kml \ No newline at end of file +*.kml diff --git a/.travis.yml b/.travis.yml index ec652eb..1a654c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: python python: + - "2.6" - "2.7" + - "3.2" + - "3.3" + - "3.4" # command to install dependencies install: diff --git a/examples/example.py b/examples/example.py index 8b0c4ef..588dabe 100644 --- a/examples/example.py +++ b/examples/example.py @@ -5,6 +5,6 @@ pts = [] for x in range(400): pts.append((random.random(), random.random())) - print "Processing %d points..." % len(pts) + print("Processing %d points..." % len(pts)) hm = heatmap.Heatmap() img = hm.heatmap(pts).save("classic.png") diff --git a/heatmap/__init__.py b/heatmap/__init__.py index da77f06..10fd00a 100644 --- a/heatmap/__init__.py +++ b/heatmap/__init__.py @@ -1,6 +1,6 @@ try: __version__ = __import__('pkg_resources').get_distribution(__name__).version -except Exception, e: +except Exception as e: __version__ = 'unknown' -from heatmap import Heatmap \ No newline at end of file +from .heatmap import Heatmap diff --git a/heatmap/heatmap.c b/heatmap/heatmap.c index f85e19a..1d84da1 100644 --- a/heatmap/heatmap.c +++ b/heatmap/heatmap.c @@ -3,6 +3,9 @@ #include #include +float constant = 50.0; +float multiplier = 200.0; + struct info { float minX; @@ -12,6 +15,7 @@ struct info int width; int height; + int cPixels; int dotsize; }; @@ -33,7 +37,7 @@ BOOL WINAPI DllMain( HINSTANCE hinstDLL, // handle to DLL module #endif //walk the list of points, get the boundary values -void getBounds(struct info *inf, float *points, unsigned int cPoints) +void getBounds(struct info *inf, float *points, unsigned int cPoints, int weighted) { unsigned int i = 0; @@ -43,8 +47,11 @@ void getBounds(struct info *inf, float *points, unsigned int cPoints) float maxX = points[i]; float maxY = points[i+1]; + int inc = 2; + if (weighted) inc = 3; + //then iterate over the list and find the max/min values - for(i = 0; i < cPoints; i=i+2) + for(i = 0; i < cPoints; i=i+inc) { float x = points[i]; float y = points[i+1]; @@ -86,17 +93,18 @@ struct point translate(struct info *inf, struct point pt) return pt; } -unsigned char* calcDensity(struct info *inf, float *points, int cPoints) +unsigned char* calcDensity(struct info *inf, float *points, int cPoints, int weighted) { int width = inf->width; int height = inf->height; + int cPixels = inf->cPixels; int dotsize = inf->dotsize; - unsigned char* pixels = (unsigned char *)calloc(width*height, sizeof(char)); + unsigned char* pixels = (unsigned char *)malloc(cPixels*sizeof(char)); float midpt = dotsize / 2.f; - double radius = sqrt(midpt*midpt + midpt*midpt) / 2.f; - double dist = 0.0; + float radius = sqrt(midpt*midpt + midpt*midpt) / 2.f; + float dist = 0.0; int pixVal = 0; int j = 0; int k = 0; @@ -104,13 +112,17 @@ unsigned char* calcDensity(struct info *inf, float *points, int cPoints) int ndx = 0; struct point pt = {0}; + int inc = 2; + if (weighted) inc = 3; + // initialize image data to white - for(i = 0; i < (int)width*height; i++) + for(i = 0; i < cPixels; i++) { pixels[i] = 0xff; } - for(i = 0; i < cPoints; i=i+2) + + for(i = 0; i < cPoints; i=i+inc) { pt.x = points[i]; pt.y = points[i+1]; @@ -123,12 +135,21 @@ unsigned char* calcDensity(struct info *inf, float *points, int cPoints) if (j < 0 || k < 0 || j >= width || k >= height) continue; dist = sqrt( (j-pt.x)*(j-pt.x) + (k-pt.y)*(k-pt.y) ); - - pixVal = (int)(200.0*(dist/radius)+50.0); - if (pixVal > 255) pixVal = 255; + + if(dist>radius) continue; // stop point contributing to pixels outside its radius ndx = k*width + j; - if(ndx >= (int)width*height) continue; // ndx can be greater than array bounds + if(ndx >= cPixels) continue; // ndx can be greater than array bounds + + if(weighted) + { + pixVal = (int)((multiplier*(dist/radius)+constant)/points[i+2]); + } + else + { + pixVal = (int)(multiplier*(dist/radius)+constant); + } + if (pixVal > 255) pixVal = 255; #ifdef DEBUG printf("pt.x: %.2f pt.y: %.2f j: %d k: %d ndx: %d\n", pt.x, pt.y, j, k, ndx); @@ -145,16 +166,14 @@ unsigned char* calcDensity(struct info *inf, float *points, int cPoints) unsigned char *colorize(struct info *inf, unsigned char* pixels_bw, int *scheme, unsigned char* pixels_color, int opacity) { - int width = inf->width; - int height = inf->height; - int dotsize = inf->dotsize; + int cPixels = inf->cPixels; int i = 0; int pix = 0; int highCount = 0; int alpha = opacity; - for(i = 0; i < (int)width*height; i++) + for(i = 0; i < cPixels; i++) { pix = pixels_bw[i]; @@ -170,7 +189,7 @@ unsigned char *colorize(struct info *inf, unsigned char* pixels_bw, int *scheme, pixels_color[i*4+3] = alpha; } - if (highCount > width*height*0.8) + if (highCount > cPixels*0.8) { fprintf(stderr, "Warning: 80%% of output pixels are over 95%% density.\n"); fprintf(stderr, "Decrease dotsize or increase output image resolution?\n"); @@ -190,23 +209,24 @@ unsigned char *tx(float *points, unsigned char *pix_color, int opacity, int boundsOverride, - float minX, float minY, float maxX, float maxY) + float minX, float minY, float maxX, float maxY, int weighted) { unsigned char *pixels_bw = NULL; + struct info inf = {0}; //basic sanity checks to keep from segfaulting if (NULL == points || NULL == scheme || NULL == pix_color || - w <= 0 || h <= 0 || cPoints <= 1 || opacity < 0 || dotsize <= 0) + w <= 0 || h <= 0 || cPoints <= 1+weighted || cPoints % (2+weighted) != 0 || + opacity < 0 || opacity > 255 || dotsize <= 0) { fprintf(stderr, "Invalid parameter; aborting.\n"); return NULL; } - struct info inf = {0}; - inf.dotsize = dotsize; inf.width = w; inf.height = h; + inf.cPixels = w*h; // get min/max x/y values from point list if (boundsOverride == 1) @@ -216,7 +236,7 @@ unsigned char *tx(float *points, } else { - getBounds(&inf, points, cPoints); + getBounds(&inf, points, cPoints, weighted); } #ifdef DEBUG @@ -225,7 +245,7 @@ unsigned char *tx(float *points, //iterate through points, place a dot at each center point //and set pix value from 0 - 255 using multiply method for radius [dotsize]. - pixels_bw = calcDensity(&inf, points, cPoints); + pixels_bw = calcDensity(&inf, points, cPoints, weighted); //using provided color scheme and opacity, update pixel value to RGBA values pix_color = colorize(&inf, pixels_bw, scheme, pix_color, opacity); diff --git a/heatmap/heatmap.py b/heatmap/heatmap.py index 173e5cc..89de89f 100644 --- a/heatmap/heatmap.py +++ b/heatmap/heatmap.py @@ -3,16 +3,22 @@ import ctypes import platform import math - -import colorschemes - +from . import colorschemes from PIL import Image +import glob + +use_pyproj = False +try: + import pyproj + use_pyproj = True +except: + pass class Heatmap: """ - Create heatmaps from a list of 2D coordinates. + Create heatmaps from a list of 2D coordinates with optional weighting per coordinate pair. - Heatmap requires the Python Imaging Library and Python 2.5+ for ctypes. + Heatmap requires the Python Imaging Library and Python 2.6+ for Python3 backports. Coordinates autoscale to fit within the image dimensions, so if there are anomalies or outliers in your dataset, results won't be what you expect. You @@ -23,6 +29,9 @@ class Heatmap: are lat/long coordinates. Make your own wardriving maps or visualize the footprint of your wireless network. + For accurate geospatial results it is advised to use the optional [proj] install. + This also allows for output in other coordinate systems such as Mercator. + Most of the magic starts in heatmap(), see below for description of that function. """ @@ -45,8 +54,6 @@ class Heatmap: """ def __init__(self, libpath=None): - self.minXY = () - self.maxXY = () self.img = None # if you're reading this, it's probably because this # hacktastic garbage failed. sorry. I deserve a jab or two via @jjguy. @@ -58,7 +65,7 @@ def __init__(self, libpath=None): # establish the right library name, based on platform and arch. Windows # are pre-compiled binaries; linux machines are compiled during setup. self._heatmap = None - libname = "cHeatmap.so" + libname = "cHeatmap" if "cygwin" in platform.system().lower(): libname = "cHeatmap.dll" if "windows" in platform.system().lower(): @@ -68,33 +75,65 @@ def __init__(self, libpath=None): # now rip through everything in sys.path to find 'em. Should be in site-packages # or local dir for d in sys.path: - if os.path.isfile(os.path.join(d, libname)): + if os.path.isfile(os.path.join(d, libname+'.so')): self._heatmap = ctypes.cdll.LoadLibrary( - os.path.join(d, libname)) + os.path.join(d, libname+'.so')) + # check for cpython-*.so prefix for object files which seems to be the ones + # copied on install in the travis python3 environment (even with the same version of setuptools) + # may investigate further and do the test based on execution environment + if not self._heatmap: + for d in sys.path: + file = glob.glob(os.path.join(d,libname+'.cpython-*.so')) + if file: + self._heatmap = ctypes.cdll.LoadLibrary(file[0]) if not self._heatmap: raise Exception("Heatmap shared library not found in PYTHONPATH.") - def heatmap(self, points, dotsize=150, opacity=128, size=(1024, 1024), scheme="classic", area=None): + def heatmap(self, points, dotsize=150, opacity=128, size=(1024, 1024), scheme="classic", area=None, + weighted=0, srcepsg=None, dstepsg='EPSG:3857'): """ - points -> an iterable list of tuples, where the contents are the - x,y coordinates to plot. e.g., [(1, 1), (2, 2), (3, 3)] - dotsize -> the size of a single coordinate in the output image in - pixels, default is 150px. Tweak this parameter to adjust - the resulting heatmap. - opacity -> the strength of a single coordiniate in the output image. - Tweak this parameter to adjust the resulting heatmap. - size -> tuple with the width, height in pixels of the output PNG - scheme -> Name of color scheme to use to color the output image. - Use schemes() to get list. (images are in source distro) - area -> Specify bounding coordinates of the output image. Tuple of - tuples: ((minX, minY), (maxX, maxY)). If None or unspecified, - these values are calculated based on the input data. + points -> A representation of the points (x,y values) to process. + Can be a flattened array/tuple or any combination of 2 dimensional + array or tuple iterables i.e. [x1,y1,x2,y2], [(x1,y1),(x2,y2)], etc. + If weights are being used there are expected to be 3 'columns' + in the 2 dimensionable iterable or a multiple of 3 points in the + flat array/tuple i.e. (x1,y1,z1,x2,y2,z2), ([x1,y1,z1],[x2,y2,z2]) etc. + The third (weight) value can be anything but it is + best to have a normalised weight between 0 and 1. + For best performance, if convenient use a flattened array + as this is what is used internally and requires no conversion. + dotsize -> the size of a single coordinate in the output image in + pixels, default is 150px. Tweak this parameter to adjust + the resulting heatmap. + opacity -> the strength of a single coordiniate in the output image. + Tweak this parameter to adjust the resulting heatmap. + size -> tuple with the width, height in pixels of the output PNG + scheme -> Name of color scheme to use to color the output image. + Use schemes() to get list. (images are in source distro) + area -> Specify bounding coordinates of the output image. Tuple of + tuples: ((minX, minY), (maxX, maxY)). If None or unspecified, + these values are calculated based on the input data. + weighted -> Is the data weighted (0 or 1) + srcepsg -> epsg code of the source, set to None to ignore. + If using KML output make sure this is set otherwise either the image + or the overlay coordinates will be out. + dstepsg -> epsg code of the destination, ignored if srcepsg is not set. + Defaults to EPSG:3857 (Cylindrical Mercator). + Due to linear interpolation in heatmap.c it only makes sense to use linear + output projections. If outputting to KML for google earth client overlay use + EPSG:4087 (World Equidistant Cylindrical). """ self.dotsize = dotsize self.opacity = opacity self.size = size self.points = points + self.weighted = weighted + self.srcepsg = srcepsg + self.dstepsg = dstepsg + + if self.srcepsg and not use_pyproj: + raise Exception('srcepsg entered but pyproj is not available') if area is not None: self.area = area @@ -103,21 +142,28 @@ def heatmap(self, points, dotsize=150, opacity=128, size=(1024, 1024), scheme="c self.area = ((0, 0), (0, 0)) self.override = 0 + #convert area for heatmap.c if required + ((east, south), (west, north)) = self.area + if use_pyproj and self.srcepsg is not None and self.srcepsg != self.dstepsg: + source = pyproj.Proj(init=self.srcepsg) + dest = pyproj.Proj(init=self.dstepsg) + (east,south) = pyproj.transform(source,dest,east,south) + (west,north) = pyproj.transform(source,dest,west,north) + if scheme not in self.schemes(): tmp = "Unknown color scheme: %s. Available schemes: %s" % ( scheme, self.schemes()) raise Exception(tmp) - arrPoints = self._convertPoints(points) + arrPoints = self._convertPoints() arrScheme = self._convertScheme(scheme) arrFinalImage = self._allocOutputBuffer() ret = self._heatmap.tx( - arrPoints, len(points) * 2, size[0], size[1], dotsize, + arrPoints, len(arrPoints), size[0], size[1], dotsize, arrScheme, arrFinalImage, opacity, self.override, - ctypes.c_float(self.area[0][0]), ctypes.c_float( - self.area[0][1]), - ctypes.c_float(self.area[1][0]), ctypes.c_float(self.area[1][1])) + ctypes.c_float(east), ctypes.c_float(south), + ctypes.c_float(west), ctypes.c_float(north), weighted) if not ret: raise Exception("Unexpected error during processing.") @@ -129,43 +175,52 @@ def heatmap(self, points, dotsize=150, opacity=128, size=(1024, 1024), scheme="c def _allocOutputBuffer(self): return (ctypes.c_ubyte * (self.size[0] * self.size[1] * 4))() - def _convertPoints(self, pts): + def _convertPoints(self): """ flatten the list of tuples, convert into ctypes array """ - #TODO is there a better way to do this?? - flat = [] - for i, j in pts: - flat.append(i) - flat.append(j) - #build array of input points - arr_pts = (ctypes.c_float * (len(pts) * 2))(*flat) + if isinstance(self.points,tuple): + self.points = list(self.points) + if isinstance(self.points[0],tuple): + self.points = list(sum(self.points,())) + elif isinstance(self.points[0],list): + self.points = sum(self.points,[]) + + #convert if required, need to copy as may use points later for _range. + if use_pyproj and self.srcepsg is not None and self.srcepsg != self.dstepsg: + converted =list(self.points) + source = pyproj.Proj(init=self.srcepsg) + dest = pyproj.Proj(init=self.dstepsg) + #nicer way? map/lambda will retun 2/3 tuple so need to flatten again + inc = 3 if self.weighted else 2 + for i in range(0, len(self.points), inc): + (x,y) = pyproj.transform(source,dest,self.points[i],self.points[i+1]) + converted[i] = x + converted[i+1] = y + arr_pts = (ctypes.c_float * (len(converted))) (*converted) + else: + arr_pts = (ctypes.c_float * (len(self.points))) (*self.points) return arr_pts def _convertScheme(self, scheme): """ flatten the list of RGB tuples, convert into ctypes array """ - #TODO is there a better way to do this?? - flat = [] - for r, g, b in colorschemes.schemes[scheme]: - flat.append(r) - flat.append(g) - flat.append(b) - arr_cs = ( - ctypes.c_int * (len(colorschemes.schemes[scheme]) * 3))(*flat) + flat = list(sum(colorschemes.schemes[scheme],())) + arr_cs = (ctypes.c_int * (len(flat)))(*flat) return arr_cs - def _ranges(self, points): + def _ranges(self): """ walks the list of points and finds the max/min x & y values in the set """ - minX = points[0][0] - minY = points[0][1] + minX = self.points[0] + minY = self.points[1] maxX = minX maxY = minY - for x, y in points: - minX = min(x, minX) - minY = min(y, minY) - maxX = max(x, maxX) - maxY = max(y, maxY) + inc = 3 if self.weighted else 2 + for i in range(0,len(self.points),inc): + minX = min(self.points[i], minX) + minY = min(self.points[i+1], minY) + maxX = max(self.points[i], maxX) + maxY = max(self.points[i+1], maxY) return ((minX, minY), (maxX, maxY)) @@ -184,12 +239,21 @@ def saveKML(self, kmlFile): self.img.save(tilePath) if self.override: - ((east, south), (west, north)) = self.area + ((west, south), (east, north)) = self.area else: - ((east, south), (west, north)) = self._ranges(self.points) + ((west, south), (east, north)) = self._ranges() + + #convert overlay BBOX if required + if use_pyproj and self.srcepsg is not None and self.srcepsg != 'EPSG:4326': + source = pyproj.Proj(init=self.srcepsg) + dest = pyproj.Proj(init='EPSG:4326') + (east,south) = pyproj.transform(source,dest,east,south) + (west,north) = pyproj.transform(source,dest,west,north) bytes = self.KML % (tilePath, north, south, east, west) - file(kmlFile, "w").write(bytes) + fh = open(kmlFile, "w") + fh.write(bytes) + fh.close() def schemes(self): """ diff --git a/requirements.txt b/requirements.txt index f917857..2ed43c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ Pillow>=2.1.0 +pyproj diff --git a/setup.py b/setup.py index 4ed87d7..e7ba458 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,22 @@ import os import glob -from distutils.core import setup, Extension -from distutils.command.install import install -from distutils.command.build_ext import build_ext +#here use a flag so don't automatically use setuptools if available, hard to test otherwise +with_setuptools = False +if 'USE_SETUPTOOLS' in os.environ or 'pip' in __file__ or 'easy_install' in __file__: + try: + from setuptools.command.install import install + from setuptools import setup + from setuptools import Extension + from setuptools.command.build_ext import build_ext + with_setuptools = True + except ImportError: + pass +if not with_setuptools: + from distutils.command.install import install + from distutils.core import setup + from distutils.core import Extension + from distutils.command.build_ext import build_ext # sorry for this, welcome feedback on the "right" way. # shipping pre-compiled bainries on windows, have @@ -12,11 +25,10 @@ class mybuild(build_ext): def run(self): if "nt" in os.name: - print "On Windows, skipping build_ext." + print("On Windows, skipping build_ext.") return build_ext.run(self) - class post_install(install): def run(self): install.run(self) @@ -32,19 +44,40 @@ def run(self): cHeatmap = Extension('cHeatmap', sources=['heatmap/heatmap.c', ]) -setup(name='heatmap', - version="2.2.1", - description='Module to create heatmaps', - author='Jeffrey J. Guy', - author_email='jjg@case.edu', - url='http://jjguy.com/heatmap/', - license='MIT License', - packages=['heatmap', ], - py_modules=['heatmap.colorschemes', ], - ext_modules=[cHeatmap, ], - cmdclass={'install': post_install, +#separate calls to remove errors +basekw = { + 'name' : 'heatmap', + 'version' : "2.2.1", + 'description' : 'Module to create heatmaps', + 'author' : 'Jeffrey J. Guy', + 'author_email' : 'jjg@case.edu', + 'url' : 'http://jjguy.com/heatmap/', + 'packages' : ['heatmap', ], + 'py_modules' : ['heatmap.colorschemes', ], + 'ext_modules' : [cHeatmap, ], + 'cmdclass' : {'install': post_install, 'build_ext': mybuild}, - requires=["Pillow", ], - test_suite="test", - tests_require=["Pillow", ], - ) + 'classifiers' : [ + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Operating System :: OS Independent', + 'License :: OSI Approved :: MIT License', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Scientific/Engineering :: Visualization', + 'Topic :: Scientific/Engineering :: GIS' + ] + } +setuptoolskw = { + 'install_requires' : ['Pillow'], + 'extras_require' : {'proj' : 'pyproj'}, + 'test_suite' : "test", + 'tests_require' : ['pyproj'] + } +distutilskw = { + 'requires' : ["Pillow"] + } + +basekw.update(setuptoolskw) if with_setuptools else basekw.update(distutilskw) +setup(**basekw) diff --git a/test/tests.py b/test/tests.py index 8720d38..15c9c36 100644 --- a/test/tests.py +++ b/test/tests.py @@ -12,61 +12,175 @@ class TestHeatmap(unittest.TestCase): """unittests for TestHeatmap""" + + def heatmapImage(self,name,pts,kwargs={},saveKML=False): + img = self.heatmap.heatmap(pts, **kwargs) + self.assertTrue(isinstance(img, Image.Image)) + if (saveKML): + self.heatmap.saveKML(name+".kml") + else: + img.save(name+".png") + return img def setUp(self): self.heatmap = heatmap.Heatmap() def test_heatmap_random_defaults(self): pts = [(random.random(), random.random()) for x in range(400)] - img = self.heatmap.heatmap(pts) - img.save("01-400-random.png") - self.assertTrue(isinstance(img, Image.Image)) + self.heatmapImage("01-400-random", pts) def test_heatmap_vert_line(self): pts = [(50, x) for x in range(100)] - img = self.heatmap.heatmap(pts, area=((0, 0), (200, 200))) - img.save("02-vert-line.png") - self.assertTrue(isinstance(img, Image.Image)) + self.heatmapImage("02-vert-line", pts, kwargs={"area" : ((0, 0), (200, 200))}) def test_heatmap_horz_line(self): pts = [(x, 300) for x in range(600, 700)] - img = self.heatmap.heatmap(pts, size=(800,400), area=((0, 0), (800, 400))) - img.save("03-horz-line.png") - self.assertTrue(isinstance(img, Image.Image)) + self.heatmapImage("03-horz-line", pts, kwargs={ "size" : (800,400), "area" : ((0, 0), (800, 400))}) def test_heatmap_random(self): - pts = [(random.random(), random.random()) for x in range(40000)] # this should also generate a warning on stderr of overly dense - img = self.heatmap.heatmap(pts, dotsize=25, opacity=128) - img.save("04-40k-random.png") - self.assertTrue(isinstance(img, Image.Image)) + pts = [(random.random(), random.random()) for x in range(40000)] + self.heatmapImage("04-40k-random", pts , kwargs={ "dotsize" : 25, "opacity" : 128}) def test_heatmap_square(self): pts = [(x*100, 50) for x in range(2, 50)] pts.extend([(4850, x*100) for x in range(2, 50)]) pts.extend([(x*100, 4850) for x in range(2, 50)]) pts.extend([(50, x*100) for x in range(2, 50)]) - img = self.heatmap.heatmap(pts, dotsize=100, area=((0,0), (5000, 5000))) - img.save("05-square.png") - self.assertTrue(isinstance(img, Image.Image)) + self.heatmapImage("05-square", pts , kwargs={ "dotsize" : 100, "area" : ((0,0), (5000, 5000)) }) def test_heatmap_single_point(self): pts = [(random.uniform(-77.012, -77.050), random.uniform(38.888, 38.910)) for x in range(100)] - img = self.heatmap.heatmap(pts) - self.heatmap.saveKML("06-wash-dc.kml") - self.assertTrue(isinstance(img, Image.Image)) + self.heatmapImage("06-wash-dc", pts) + + def test_heatmap_weighted(self): + #normal should be the same as 100%, 75% should have same pattern but smaller + pts = [(random.uniform(30,40), random.uniform(-30,-40)) for x in range(400)] + norm = self.heatmapImage("07-400-normal", pts, saveKML=True) + weight = self.heatmapImage("07-400-100percent", list(map( lambda x_y : (x_y[0],x_y[1],1), pts)), kwargs={ "weighted" : 1 }, saveKML=True) + self.assertEqual(norm,weight) + weight2 = self.heatmapImage("07-400-75percent", list(map( lambda x_y : (x_y[0],x_y[1],.75), pts)), kwargs={ "weighted" : 1 }, saveKML=True) + self.assertNotEqual(norm,weight2) + + def test_heatmap_random_datatypes(self): + #all of the below should turn out to be the same, if not there are issues + pts = tuple((random.random(),random.random(),1) for x in range(400)) + tt = self.heatmapImage("08-400-tupleoftuples", tuple(map(lambda x_y_z : (x_y_z[0],x_y_z[1]), pts)), saveKML=True) + ttw = self.heatmapImage("08-400-tupleoftuplesweighted", pts , kwargs = { "weighted" : True }, saveKML=True) + at = self.heatmapImage("08-400-arrayoftuples", list(map(lambda x_y_z : (x_y_z[0],x_y_z[1]), pts)), saveKML=True) + atw = self.heatmapImage("08-400-arrayoftuplesweighted", list(pts), kwargs = { "weighted" : True }, saveKML=True) + ta = self.heatmapImage("08-400-tupleofarrays", tuple(map(lambda x_y_z : [x_y_z[0],x_y_z[1]], pts)), saveKML=True) + taw = self.heatmapImage("08-400-tupleofarraysweighted", tuple(map(lambda x_y_z : [x_y_z[0],x_y_z[1],x_y_z[2]], pts)), kwargs = { "weighted" : True }, saveKML=True) + aa = self.heatmapImage("08-400-arrayofarrays", list(map(lambda x_y_z : [x_y_z[0],x_y_z[1]], pts)), saveKML=True) + aaw = self.heatmapImage("08-400-arrayofarraysweighted", list(map(lambda x_y_z : [x_y_z[0],x_y_z[1],x_y_z[2]], pts)), kwargs = { "weighted" : True }, saveKML=True) + f = self.heatmapImage("08-400-flat", sum(map(lambda x_y_z : [x_y_z[0],x_y_z[1]], pts),[]), saveKML=True) + fw = self.heatmapImage("08-400-flatweighted", sum(map(lambda x_y_z : [x_y_z[0],x_y_z[1],x_y_z[2]], pts),[]), kwargs = { "weighted" : True }, saveKML=True) + self.assertEqual(tt,ttw) + self.assertEqual(tt,at) + self.assertEqual(tt,atw) + self.assertEqual(tt,ta) + self.assertEqual(tt,taw) + self.assertEqual(tt,aa) + self.assertEqual(tt,aaw) + self.assertEqual(tt,f) + self.assertEqual(tt,fw) + + def test_heatmap_area(self): + MAX_SIZE=8192 + PPD=100 + dotsize=100 + pts = [[x*2,x, 1 if x==0 else 0.75] for x in range(-45,46)] + pts = sum(pts,[]) + west = pts[0] + south = pts[1] + east = west + north = south + for i in range(0,len(pts),3): + west = min(pts[i], west) + south = min(pts[i+1], south) + east = max(pts[i], east) + north = max(pts[i+1], north) + width = int((east - west)*PPD + dotsize/2) + height = int((north - south)*PPD + dotsize/2) + largestVal = max(width,height) + if largestVal > MAX_SIZE: + scale = float(MAX_SIZE)/largestVal + height = int(height*scale) + width = int(width*scale) + PPD = float((width-dotsize/2))/(east-west) + dotDegrees = dotsize/2/PPD + bounds = ((west-dotDegrees, south-dotDegrees),(east+dotDegrees,north+dotDegrees)) + #these should be the same except less cutting off the corners with the manual area + self.heatmapImage("11-400-areaTest", pts, { "size" : (width, height), "dotsize" : dotsize, "area" : bounds, "weighted" : 1}, saveKML = True) + self.heatmapImage("11-400-areaTestNormal", pts , kwargs = { "size" : (width, height), "dotsize" : dotsize, "weighted" : 1}, saveKML = True) + + def test_heatmap_random_proj(self): + pts = [(random.uniform(-180,180),random.uniform(-90,90)) for x in range(400)] + norm = self.heatmapImage("09-400-normal", pts, saveKML = True) + #4087 should be the same as 'normal' as no conversion required, kml boundary should be different (not tested) though as not converted to 4326 + epsg4087 = self.heatmapImage("09-400-EPSG4087", pts, kwargs = { "srcepsg" : "EPSG:4087", "dstepsg" : "EPSG:4087"}, saveKML = True) + self.assertEqual(norm,epsg4087) + #4326 should be roughly the same as 'normal' but not the same as WGS84 != 4087 + epsg4326 = self.heatmapImage("09-400-EPSG4326", pts, kwargs = { "srcepsg" : "EPSG:4326", "dstepsg" : "EPSG:4087" }, saveKML = True) + self.assertNotEqual(norm,epsg4326) + #3857DST should be well different + epsg3857DST = self.heatmapImage("09-400-EPSG3857DST", pts, kwargs = { "srcepsg" : "EPSG:4326"}, saveKML = True) + self.assertNotEqual(norm,epsg3857DST) + #testing conversion of src epsg, image is possibly similar do to linearity at the equator but KML boundary should be very different (not tested) + epsg3857 = self.heatmapImage("09-400-EPSG3857", pts, kwargs = { "srcepsg" : "EPSG:3857", "dstepsg" : "EPSG:4087" }, saveKML = True) + + def test_heatmap_weighted_proj(self): + pts = [(x*2,x, 1 if x==0 else 0.75) for x in range(-89,90)] + norm = self.heatmapImage("10-400-normal", pts, kwargs = { "size" : (2048, 1024), "dotsize" : 50, "weighted" : 1}, saveKML = True) + #4087 should be the same as 'normal' as no conversion required, kml boundary should be different (not tested) though as not converted to 4326 + epsg4087 = self.heatmapImage("10-400-EPSG4087", pts, kwargs = { "srcepsg" : "EPSG:4087", "dstepsg" : "EPSG:4087", "size" : (2048, 1024), "dotsize" : 50, "weighted" : 1}, saveKML = True) + self.assertEqual(norm,epsg4087) + #4326 should be roughly the same as 'normal' but not the same as WGS84 != 4087 + norm = self.heatmapImage("10-400-normal", pts, kwargs = { "size" : (2048, 1024), "dotsize" : 50, "weighted" : 1}, saveKML = True) + epsg4326 = self.heatmapImage("10-400-EPSG4326SRC", pts, kwargs = { "srcepsg" : "EPSG:4326", "dstepsg" : "EPSG:4087", "size" : (2048, 1024), "dotsize" : 50, "weighted" : 1}, saveKML = True) + self.assertNotEqual(norm,epsg4326) + #3857DST and 4087 should roughly meet at 0,0 as symetrical around the equator + epsg3857DST = self.heatmapImage("10-400-EPSG3857DST", pts, kwargs = { "srcepsg" : "EPSG:4326", "size" : (2048, 1024), "dotsize" : 50, "weighted" : 1}, saveKML = True) + self.assertNotEqual(norm,epsg3857DST) + #testing conversion of src epsg, image is possibly similar do to linearity at the equator but KML boundary should be very different (not tested) + epsg3857 = self.heatmapImage("10-400-EPSG3857", pts, kwargs = { "srcepsg" : "EPSG:3857", "dstepsg" : "EPSG:4087", "size" : (2048, 1024), "dotsize" : 50, "weighted" : 1 }, saveKML = True) + + def test_heatmap_exceptions(self): + + #test invalid (empty) heatmap, should print error to stdout + emptyHeatmapArgs = [Exception, 'Unexpected error', self.heatmap.heatmap, ([],)] + emptyHeatmapKwargs = {} + #test invalid scheme + invalidColorSchemeArgs = [Exception, 'Unknown color scheme', self.heatmap.heatmap, ([],)] + invalidColourSchemeKwargs = {'scheme' : 'invalid'} + #test saveKML before create heatmap + saveKMLArgs = [Exception, 'Must first run heatmap', self.heatmap.saveKML,"test.kml"] + saveKMLKwargs = {} - def test_invalid_heatmap(self): - self.assertRaises(Exception, self.heatmap.heatmap, ([],)) + #better way? no __verison__ in unittest + try: + function = self.assertRaisesRegex + except: + try: + function = self.assertRaisesRegexp + except: + function = self.assertRaises + emptyHeatmapArgs.pop(1) + invalidColorSchemeArgs.pop(1) + saveKMLArgs.pop(1) + + function(*emptyHeatmapArgs, **emptyHeatmapKwargs) + function(*invalidColorSchemeArgs, **invalidColourSchemeKwargs) + function(*saveKMLArgs, **saveKMLKwargs) class TestColorScheme(unittest.TestCase): def test_schemes(self): keys = colorschemes.valid_schemes() - self.assertEqual(keys, ['fire', 'pgaitch', 'pbj', 'omg', 'classic']) + self.assertEqual(sorted(list(keys)), sorted(['fire', 'pgaitch', 'pbj', 'omg', 'classic'])) def test_values(self): - for key, values in colorschemes.schemes.iteritems(): + for key, values in colorschemes.schemes.items(): self.assertTrue(isinstance(values, list)) self.assertEqual(len(values), 256) for value in values: