Skip to content

Commit

Permalink
Add image_overlay()
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewgiessel committed Jul 30, 2015
1 parent a7fd9b9 commit 25734fb
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 62 deletions.
9 changes: 4 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
language: python

sudo: false

env:
- CONDA="python=2.7"
- CONDA="python=3.3"
- CONDA="python=3.4"

before_install:
- URL=http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh
- wget $URL -O miniconda.sh
- wget http://bit.ly/miniconda -O miniconda.sh
- bash miniconda.sh -b -p $HOME/miniconda
- export PATH="$HOME/miniconda/bin:$PATH"
- hash -r
- conda config --set always_yes yes
- conda update conda
- conda update --yes conda
- conda info -a
- travis_retry conda create -n test $CONDA pip jinja2 pandas mock six nose
- source activate test
- travis_retry pip install vincent

install:
- export PYTHONWARNINGS=all
- python setup.py install

script:
Expand Down
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Added cartodb positron and dark_matter tiles (ocefpaf d4daee7)
- Forcing HTTPS when available. (ocefpaf c69ac89)
- Added Stamen Watercolor tiles. (ocefpaf 8c1f837)
- Added Image Overlay. (andrewgiessel ---)

Bug Fixes

Expand Down
104 changes: 94 additions & 10 deletions folium/folium.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from folium.six import text_type, binary_type, iteritems

import sys

import base64

ENV = Environment(loader=PackageLoader('folium', 'templates'))

Expand Down Expand Up @@ -221,6 +221,7 @@ def __init__(self, location=None, width='100%', height='100%',
self.added_layers = []
self.template_vars.setdefault('wms_layers', [])
self.template_vars.setdefault('tile_layers', [])
self.template_vars.setdefault('image_layers', [])

@iter_obj('simple')
def add_tile_layer(self, tile_name=None, tile_url=None, active=False):
Expand Down Expand Up @@ -281,18 +282,14 @@ def add_layers_to_map(self):
data_string = ''
for i, layer in enumerate(self.added_layers):
name = list(layer.keys())[0]
data_string += '\"'
data_string += name
data_string += '\"'
data_string += ': '
data_string += name
if i < len(self.added_layers)-1:
data_string += ",\n"
term_string = ",\n"
else:
data_string += "\n"
term_string += "\n"
data_string += '\"{}\": {}'.format(name, name, term_string)

data_layers = layers_temp.render({'layers': data_string})
self.template_vars.setdefault('data_layers', []).append((data_string))
self.template_vars.setdefault('data_layers', []).append((data_layers))

@iter_obj('simple')
def simple_marker(self, location=None, popup=None,
Expand Down Expand Up @@ -647,7 +644,8 @@ def fit_bounds(self, bounds, padding_top_left=None,
fit_bounds = self.env.get_template('fit_bounds.js')
fit_bounds_str = fit_bounds.render({
'bounds': json.dumps(bounds),
'fit_bounds_options': json.dumps(fit_bounds_options),
'fit_bounds_options': json.dumps(fit_bounds_options,
sort_keys=True),
})

self.template_vars.update({'fit_bounds': fit_bounds_str})
Expand Down Expand Up @@ -948,6 +946,92 @@ def json_style(style_cnt, line_color, line_weight, line_opacity,
self.template_vars.setdefault('geo_styles', []).append(style)
self.template_vars.setdefault('gjson_layers', []).append(layer)

@iter_obj('image_overlay')
def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
min_lon=-180.0, max_lon=180.0, image_name=None, filename=None):
"""Simple image overlay of raster data from a numpy array. This is a lightweight
way to overlay geospatial data on top of a map. If your data is high res, consider
implementing a WMS server and adding a WMS layer.
This function works by generating a PNG file from a numpy array. If you do not
specifiy a filename, it will embed the image inline. Otherwise, it saves the file in the
current directory, and then adds it as an image overlay layer in leaflet.js.
By default, the image is placed and stretched using bounds that cover the
entire globe.
Parameters
----------
data: numpy array OR url string, required.
if numpy array, must be a image format, i.e., NxM (mono), NxMx3 (rgb), or NxMx4 (rgba)
if url, must be a valid url to a image (local or external)
opacity: float, default 0.25
Image layer opacity in range 0 (completely transparent) to 1 (opaque)
min_lat: float, default -90.0
max_lat: float, default 90.0
min_lon: float, default -180.0
max_lon: float, default 180.0
image_name: string, default None
The name of the layer object in leaflet.js
filename: string, default None
Optional file name of output.png for image overlay. If None, we use a
inline PNG.
Output
------
Image overlay data layer in obj.template_vars
Examples
-------
# assumes a map object `m` has been created
>>> import numpy as np
>>> data = np.random.random((100,100))
# to make a rgba from a specific matplotlib colormap:
>>> import matplotlib.cm as cm
>>> cmapper = cm.cm.ColorMapper('jet')
>>> data2 = cmapper.to_rgba(np.random.random((100,100)))
# place the data over all of the globe (will be pretty pixelated!)
>>> m.image_overlay(data)
# put it only over a single city (Paris)
>>> m.image_overlay(data, min_lat=48.80418, max_lat=48.90970, min_lon=2.25214, max_lon=2.44731)
"""

if isinstance(data, str):
filename = data
else:
try:
png_str = utilities.write_png(data)
except Exception as e:
raise e

if filename is not None:
with open(filename, 'wb') as fd:
fd.write(png_str)
else:
filename = "data:image/png;base64,"+base64.b64encode(png_str).decode('utf-8')

if image_name not in self.added_layers:
if image_name is None:
image_name = "Image_Overlay"
else:
image_name = image_name.replace(" ", "_")
image_url = filename
image_bounds = [[min_lat, min_lon], [max_lat, max_lon]]
image_opacity = opacity

image_temp = self.env.get_template('image_layer.js')

image = image_temp.render({'image_name': image_name,
'image_url': image_url,
'image_bounds': image_bounds,
'image_opacity': image_opacity})

self.template_vars['image_layers'].append(image)
self.added_layers.append(image_name)

def _build_map(self, html_templ=None, templ_type='string'):
self._auto_bounds()
"""Build HTML/JS/CSS from Templates given current map type."""
Expand Down
7 changes: 7 additions & 0 deletions folium/templates/fol_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@

L.control.layers(baseLayer, layer_list).addTo(map);

/*
addition of the image layers
*/
{% for image in image_layers %}
{{ image }}
{% endfor %}

//cluster group
var clusteredmarkers = L.markerClusterGroup();
//section for adding clustered markers
Expand Down
1 change: 1 addition & 0 deletions folium/templates/image_layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
var {{ image_name }} = L.imageOverlay('{{ image_url }}', {{ image_bounds }}).addTo(map).setOpacity({{ image_opacity }});
75 changes: 75 additions & 0 deletions folium/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from __future__ import division
import math
from jinja2 import Environment, PackageLoader, Template
import struct, zlib

try:
import pandas as pd
Expand Down Expand Up @@ -270,3 +271,77 @@ def base(x):
# Some weirdness in series quantiles a la 0.13
arr = series.values
return [base(np.percentile(arr, x)) for x in quants]

def write_png(array):
"""Format a numpy array as a PNG byte string.
This can be writen to disk using binary I/O, or encoded using base64
for an inline png like this:
>>> png_str = write_png(array)
>>> "data:image/png;base64,"+base64.b64encode(png_str)
Taken from
http://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image
Parameters
----------
array: numpy array
Must be NxM (mono), NxMx3 (rgb) or NxMx4 (rgba)
Returns
-------
PNG formatted byte string
"""
import numpy as np

array = np.atleast_3d(array)
if array.shape[2] not in [1, 3, 4]:
raise ValueError("Data must be NxM (mono), " + \
"NxMx3 (rgb), or NxMx4 (rgba)")

# have to broadcast up into a full rgba array
array_full = np.empty((array.shape[0], array.shape[1], 4))
# NxM -> NxMx4
if array.shape[2] == 1:
array_full[:,:,0] = array[:,:,0]
array_full[:,:,1] = array[:,:,0]
array_full[:,:,2] = array[:,:,0]
array_full[:,:,3] = 1
# NxMx3 -> NxMx4
elif array.shape[2] == 3:
array_full[:,:,0] = array[:,:,0]
array_full[:,:,1] = array[:,:,1]
array_full[:,:,2] = array[:,:,2]
array_full[:,:,3] = 1
# NxMx4 -> keep
else:
array_full = array

# normalize to uint8 if it isn't already
if array_full.dtype != 'uint8':
for component in range(4):
frame = array_full[:,:,component]
array_full[:,:,component] = (frame / frame.max() * 255)
array_full = array_full.astype('uint8')
width, height = array_full.shape[:2]

array_full = array_full.tobytes()

# reverse the vertical line order and add null bytes at the start
width_byte_4 = width * 4
raw_data = b''.join(b'\x00' + array_full[span:span + width_byte_4]
for span in range((height - 1) * width * 4, -1, - width_byte_4))

def png_pack(png_tag, data):
chunk_head = png_tag + data
return (struct.pack("!I", len(data)) +
chunk_head +
struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)))

return b''.join([
b'\x89PNG\r\n\x1a\n',
png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
png_pack(b'IDAT', zlib.compress(raw_data, 9)),
png_pack(b'IEND', b'')])

0 comments on commit 25734fb

Please sign in to comment.