Tyler Erickson's Map Widget Code

This code create a Map Widget that allows the user to display zoomable, interactive maps inline in their browser. Beware -this was written for IPython 2.0.0, so if you are using a new version of Jupyter notebook you will be not be able to run this code. If you need instructions on how to set up Earth Engine, please see the Python API instructions in this repository.


In [4]:
from IPython.html import widgets
from IPython.utils import traitlets

In [2]:
class GoogleMapsWidget(widgets.DOMWidget):
    _view_name = traitlets.Unicode('GoogleMapsView', sync=True)
    value = traitlets.Unicode(sync=True)
    description = traitlets.Unicode(sync=True)
    lat = traitlets.CFloat(0, help="Center latitude, -90 to 90", sync=True)
    lng = traitlets.CFloat(0, help="Center longitude, -180 to 180", sync=True)
    zoom = traitlets.CInt(0, help="Zoom level, 0 to ~25", sync=True)
    bounds = traitlets.List([], help="Visible bounds, [W, S, E, N]", sync=True)
    
    def __init__(self, lng=0.0, lat=0.0, zoom=2):
        self.lng = lng
        self.lat = lat
        self.zoom = zoom
        
    def addLayer(self, image, vis_params, name=None, visible=True):
        mapid = image.getMapId(vis_params)
        self.send({'command':'addLayer', 'mapid': mapid['mapid'], 'token': mapid['token'], 'name': name, 'visible': visible})
        
    def center(self, lng, lat, zoom=None):
        self.send({'command': 'center', 'lng': lng, 'lat': lat, 'zoom': zoom})


/Users/catherinekuhn/miniconda2/envs/ee-python/lib/python2.7/site-packages/ipykernel/__main__.py:2: DeprecationWarning: metadata {'sync': True} was set from the constructor.  Metadata should be set using the .tag() method, e.g., Int().tag(key1='value1', key2='value2')
  from ipykernel import kernelapp as app
/Users/catherinekuhn/miniconda2/envs/ee-python/lib/python2.7/site-packages/ipykernel/__main__.py:3: DeprecationWarning: metadata {'sync': True} was set from the constructor.  Metadata should be set using the .tag() method, e.g., Int().tag(key1='value1', key2='value2')
  app.launch_new_instance()
/Users/catherinekuhn/miniconda2/envs/ee-python/lib/python2.7/site-packages/ipykernel/__main__.py:4: DeprecationWarning: metadata {'sync': True} was set from the constructor.  Metadata should be set using the .tag() method, e.g., Int().tag(key1='value1', key2='value2')
/Users/catherinekuhn/miniconda2/envs/ee-python/lib/python2.7/site-packages/ipykernel/__main__.py:5: DeprecationWarning: metadata {'sync': True} was set from the constructor.  Metadata should be set using the .tag() method, e.g., Int().tag(key1='value1', key2='value2')
/Users/catherinekuhn/miniconda2/envs/ee-python/lib/python2.7/site-packages/ipykernel/__main__.py:6: DeprecationWarning: metadata {'sync': True} was set from the constructor.  Metadata should be set using the .tag() method, e.g., Int().tag(key1='value1', key2='value2')
/Users/catherinekuhn/miniconda2/envs/ee-python/lib/python2.7/site-packages/ipykernel/__main__.py:7: DeprecationWarning: metadata {'sync': True} was set from the constructor.  Metadata should be set using the .tag() method, e.g., Int().tag(key1='value1', key2='value2')
/Users/catherinekuhn/miniconda2/envs/ee-python/lib/python2.7/site-packages/ipykernel/__main__.py:8: DeprecationWarning: metadata {'sync': True} was set from the constructor.  Metadata should be set using the .tag() method, e.g., Int().tag(key1='value1', key2='value2')

In [3]:
%%javascript

require(["widgets/js/widget"], function(WidgetManager){
    var maps = [];
    
    // Define the GoogleMapsView
    var GoogleMapsView = IPython.DOMWidgetView.extend({
        
        render: function() {
            // Resize widget element to be 100% wide
            this.$el.css('width', '100%');

            // iframe source;  just enough to load Google Maps and let us poll whether initialization is complete
            var src='<html style="height:100%"><head>' +
                '<scr'+'ipt src="http://maps.googleapis.com/maps/api/js?sensor=false"></scr'+'ipt>' +
                '<scr'+'ipt>google.maps.event.addDomListener(window,"load",function(){ready=true});</scr'+'ipt>' +
                '</head>' +
                '<body style="height:100%; margin:0px; padding:0px"></body></html>';
            
            // Create the Google Maps container element.
            this.$iframe = $('<iframe />')
                .css('width', '100%')
                .css('height', '1000px')
                .attr('srcdoc', src)
                .appendTo(this.$el);
                        
            var self = this; // hold onto this for initMapWhenReady

            // Wait until maps library has finished loading in iframe, then create map
            function initMapWhenReady() {
                // Iframe document and window
                var doc = self.$iframe[0].contentDocument;
                var win = self.$iframe[0].contentWindow;
                if (!win || !win.ready) {
                    // Maps library not yet loaded;  try again soon
                    setTimeout(initMapWhenReady, 20);
                    return;
                }

                // Maps library finished loading.  Build map now.
                var mapOptions = {
                    center: new win.google.maps.LatLng(self.model.get('lat'), self.model.get('lng')),
                    zoom: self.model.get('zoom')
                };
                var mapDiv = $('<div />')
                    .css('width', '100%')
                    .css('height', '100%')
                    .appendTo($(doc.body));
                self.map = new win.google.maps.Map(mapDiv[0], mapOptions);
                
                
                // Add an event listeners for user panning, zooming, and resizing map
                // TODO(rsargent): Bind self across all methods, and save some plumbing here
                win.google.maps.event.addListener(self.map, 'bounds_changed', function () {
                    self.handleBoundsChanged();
                });
                
                self.initializeLayersControl();
            }
            initMapWhenReady();
        },
        
        LayersControl: function(widget, controlDiv, map) {
            var win = widget.$iframe[0].contentWindow;
            var chicago = new win.google.maps.LatLng(41.850033, -87.6500523);

            // Set CSS styles for the DIV containing the control
            // Setting padding to 5 px will offset the control
            // from the edge of the map.
            controlDiv.style.padding = '5px';

            // Set CSS for the control border.
            var $controlUI = $('<div />')
                .css('backgroundColor', 'white')
                .css('borderStyle', 'solid')
                .css('borderWidth', '1px')
                .css('cursor', 'pointer')
                .css('textAlign', 'center')
                .appendTo($(controlDiv));
            
            // Set CSS for the control interior.
            var $controlContents = $('<div />')
                .css('fontFamily', 'Arial,sans-serif')
                .css('fontSize', '12px')
                .css('paddingLeft', '4px')
                .css('paddingRight', '4px')
                .css('paddingTop', '0px')
                .css('paddingBottom', '0px')
                .appendTo($controlUI);
            
            this.$controlTable = $('<table />')
                .append($('<tr><td colspan=2>Layers</td></tr>'))
                .appendTo($controlContents);
        },

        initializeLayersControl: function() {
            var doc = this.$iframe[0].contentDocument;
            var win = this.$iframe[0].contentWindow;

            // Create the DIV to hold the control and call the LayersControl() constructor
            // passing in this DIV.
    
            var layersControlDiv = document.createElement('div');
            this.layersControl = new this.LayersControl(this, layersControlDiv, this.map);

            layersControlDiv.index = 1;
            this.map.controls[win.google.maps.ControlPosition.TOP_RIGHT].push(layersControlDiv);
        },
        
        // Map geometry changed (pan, zoom, resize)
        handleBoundsChanged: function() {
            this.model.set('lng', this.map.getCenter().lng());
            this.model.set('lat', this.map.getCenter().lat());
            this.model.set('zoom', this.map.getZoom());
            var bounds = this.map.getBounds();
            var playgroundCompatible = [bounds.getSouthWest().lng(), bounds.getSouthWest().lat(),
                                        bounds.getNorthEast().lng(), bounds.getNorthEast().lat()];
            this.model.set('bounds', playgroundCompatible);
            this.touch();
        },
        
        // Receive custom messages from Python backend
        on_msg: function(msg) {
            var win = this.$iframe[0].contentWindow;
            if (msg.command == 'addLayer') {
                this.addLayer(msg.mapid, msg.token, msg.name, msg.visible);
            } else if (msg.command == 'center') {
                this.map.setCenter(new win.google.maps.LatLng(msg.lat, msg.lng));
                if (msg.zoom !== null) {
                    this.map.setZoom(msg.zoom);
                }
            }
        },
        
        // Add an Earth Engine layer
        addLayer: function(mapid, token, name, visible) {
            var win = this.$iframe[0].contentWindow;
            var eeMapOptions = {
                getTileUrl: function(tile, zoom) {
                    var url = ['https://earthengine.googleapis.com/map',
                               mapid, zoom, tile.x, tile.y].join("/");
                    url += '?token=' + token
                    return url;
                },
                tileSize: new win.google.maps.Size(256, 256),
                opacity: visible ? 1.0 : 0.0,
            };
            
            // Create the overlay map type
            var mapType = new win.google.maps.ImageMapType(eeMapOptions);
                
            // Overlay the Earth Engine generated layer
            this.map.overlayMapTypes.push(mapType);

            // Update layer visibility control
            var maxSlider = 100;
            
            function updateOpacity() {
                mapType.setOpacity($checkbox[0].checked ? $slider[0].value / 100.0 : 0);
            }
            
            var $checkbox = $('<input type="checkbox">')
                .prop('checked', visible)
                .change(updateOpacity);
            
            var $slider = $('<input type="range" />')
                .prop('min', 0)
                .prop('max', maxSlider)
                .prop('value', maxSlider)
                .css('width', '60px')
                .on('input', updateOpacity);

            // If user doesn't specify a layer name, create a default
            if (name === null) {
                name = 'Layer ' + this.map.overlayMapTypes.length;
            }
            
            var $row = $('<tr />');
            $('<td align="left" />').append($checkbox).append(name).appendTo($row);
            $('<td />').append($slider).appendTo($row);

            this.layersControl.$controlTable.append($row);
        }
    });
    
    // Register the GoogleMapsView with the widget manager.
    WidgetManager.register_widget_view('GoogleMapsView', GoogleMapsView);
});