Adding ability to select geo map from the GUI.

Change-Id: I956238500f868ef59bf947cb9f0aa7fc71d3fe84
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/MapSelectorMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/MapSelectorMessageHandler.java
new file mode 100644
index 0000000..537b288
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/MapSelectorMessageHandler.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.ui.impl;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.UiMessageHandler;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Message handler for map selection functionality.
+ */
+class MapSelectorMessageHandler extends UiMessageHandler {
+
+    private static final String MAP_LIST_REQ = "mapSelectorRequest";
+    private static final String MAP_LIST_RESP = "mapSelectorResponse";
+
+    private static final String ORDER = "order";
+    private static final String MAPS = "maps";
+    private static final String MAP_ID = "id";
+    private static final String DESCRIPTION = "description";
+    private static final String SCALE = "scale";
+
+    private static final List<Map> SUPPORTED_MAPS =
+            ImmutableList.of(new Map("australia", "Australia", 1.0),
+                             new Map("ns_america", "North, Central and South America", 0.7),
+                             new Map("s_america", "South America", 0.9),
+                             new Map("usa", "United States", 1.0),
+                             new Map("bayarea", "Bay Area, California", 1.0),
+                             new Map("europe", "Europe", 2.5),
+                             new Map("italy", "Italy", 0.8),
+                             new Map("uk", "United Kingdom and Ireland", 0.6),
+                             new Map("japan", "Japan", 0.8),
+                             new Map("s_korea", "South Korea", 0.75),
+                             new Map("taiwan", "Taiwan", 0.7),
+                             new Map("world", "World", 1.0));
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(
+                new MapListHandler()
+        );
+    }
+
+    private final class MapListHandler extends RequestHandler {
+        private MapListHandler() {
+            super(MAP_LIST_REQ);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            sendMessage(MAP_LIST_RESP, 0, mapsJson());
+        }
+    }
+
+    private ObjectNode mapsJson() {
+        ObjectNode payload = objectNode();
+        ArrayNode order = arrayNode();
+        ObjectNode maps = objectNode();
+        payload.set(ORDER, order);
+        payload.set(MAPS, maps);
+        SUPPORTED_MAPS.forEach(m -> {
+            maps.set(m.id, objectNode().put(MAP_ID, m.id)
+                    .put(DESCRIPTION, m.description)
+                    .put(SCALE, m.scale));
+            order.add(m.id);
+        });
+        return payload;
+    }
+
+    private static final class Map {
+        private final String id;
+        private final String description;
+        private final double scale;
+
+        private Map(String id, String description, double scale) {
+            this.id = id;
+            this.description = description;
+            this.scale = scale;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
index f402298..5006e76 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
@@ -137,6 +137,7 @@
                 () -> ImmutableList.of(
                         new UserPreferencesMessageHandler(),
                         new TopologyViewMessageHandler(),
+                        new MapSelectorMessageHandler(),
                         new DeviceViewMessageHandler(),
                         new LinkViewMessageHandler(),
                         new HostViewMessageHandler(),
diff --git a/web/gui/src/main/webapp/app/fw/svg/map.js b/web/gui/src/main/webapp/app/fw/svg/map.js
index a74f95d..aadb693 100644
--- a/web/gui/src/main/webapp/app/fw/svg/map.js
+++ b/web/gui/src/main/webapp/app/fw/svg/map.js
@@ -118,20 +118,21 @@
 
     function reshade(sh) {
         var p = sh && sh.palette,
-            svg, paths, stroke, fill, bg;
+            paths, stroke, fill, bg,
+            svg = d3.select('#ov-topo').select('svg');
         if (sh) {
             stroke = p.outline;
             fill = sh.flip ? p.sea : p.land;
             bg = sh.flip ? p.land : p.sea;
 
-            svg = d3.select('#ov-topo').select('svg');
             paths = d3.select('#topo-map').selectAll('path');
-
             svg.style('background-color', bg);
             paths.attr({
                 stroke: stroke,
                 fill: fill
             });
+        } else {
+            svg.style('background-color', null);
         }
     }
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index ceb0e4b..c2b81f3 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -771,3 +771,11 @@
     visibility: hidden;
 }
 
+.map-list {
+    padding: 10px;
+}
+
+.map-list select {
+    font-size: 20px;
+}
+
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index b6c83ce..7de4cd1 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -29,8 +29,8 @@
     ];
 
     // references to injected services
-    var $scope, $log, $cookies, fs, ks, zs, gs, ms, sus, flash, wss, ps, th,
-        tds, t3s, tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, tspr,
+    var $scope, $log, $cookies, $loc, fs, ks, zs, gs, ms, sus, flash, wss, ps, th,
+        tds, t3s, tes, tfs, tps, tis, tms, tss, tls, tts, tos, fltr, ttbs, tspr,
         ttip, tov;
 
     // DOM elements
@@ -53,7 +53,8 @@
             M: [toggleOffline, 'Toggle offline visibility'],
             P: [togglePorts, 'Toggle Port Highlighting'],
             dash: [tfs.showBadLinks, 'Show bad links'],
-            B: [toggleMap, 'Toggle background map'],
+            B: [toggleMap, 'Toggle background geo map'],
+            G: [openMapSelection, 'Select background geo map'],
             S: [toggleSprites, 'Toggle sprite layer'],
 
             X: [tfs.resetAllLocations, 'Reset node locations'],
@@ -78,7 +79,7 @@
             _keyListener: ttbs.keyListener,
 
             _helpFormat: [
-                ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B', 'S' ],
+                ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B', 'G', 'S' ],
                 ['X', 'Z', 'N', 'L', 'U', 'R', '-', 'E', '-', 'dot'],
                 []   // this column reserved for overlay actions
             ]
@@ -157,6 +158,10 @@
         _togSvgLayer(x, mapG, 'bg', 'background map');
     }
 
+    function openMapSelection() {
+        tms.openMapSelection();
+    }
+
     function toggleSprites(x) {
         _togSvgLayer(x, spriteG, 'spr', 'sprite layer');
     }
@@ -368,12 +373,22 @@
         } : '';
     }
 
+    function setMap(map) {
+        ps.setPrefs('topo_mapid', map);
+        setUpMap($loc);
+        opacifyMap(true);
+    }
+
+    function currentMap() {
+        return ps.getPrefs(
+            'topo_mapid',
+            { mapid: 'usa', mapscale: 1, tint: 'off'},
+            $loc.search()
+        );
+    }
+
     function setUpMap($loc) {
-        var prefs = ps.getPrefs(
-                'topo_mapid',
-                { mapid: 'usa', mapscale: 1, tint: 'off'},
-                $loc.search()
-            ),
+        var prefs = currentMap(),
             mapId = prefs.mapid,
             mapScale = prefs.mapscale,
             tint = prefs.tint,
@@ -385,7 +400,14 @@
         $log.debug('setUpMap() mapId:', mapId, ', mapScale:', mapScale,
                    ', tint:', tint);
 
-        mapG = zoomLayer.append('g').attr('id', 'topo-map');
+        mapG = d3.select('#topo-map');
+        if (mapG.empty()) {
+            mapG = zoomLayer.append('g').attr('id', 'topo-map');
+        } else {
+            mapG.each(function(d,i) {
+                d3.selectAll(this.childNodes).remove();
+            });
+        }
         if (mapId === 'usa') {
             shadeFlip = 0;
             promise = ms.loadMapInto(mapG, '*continental_us', {
@@ -508,15 +530,15 @@
             'TopoEventService', 'TopoForceService', 'TopoPanelService',
             'TopoInstService', 'TopoSelectService', 'TopoLinkService',
             'TopoTrafficService', 'TopoObliqueService', 'TopoFilterService',
-            'TopoToolbarService', 'TopoSpriteService', 'TooltipService',
-            'TopoOverlayService',
+            'TopoToolbarService', 'TopoMapService', 'TopoSpriteService',
+            'TooltipService', 'TopoOverlayService',
 
-        function (_$scope_, _$log_, $loc, $timeout, _$cookies_, _fs_, mast, _ks_,
+        function (_$scope_, _$log_, _$loc_, $timeout, _$cookies_, _fs_, mast, _ks_,
                   _zs_, _gs_, _ms_, _sus_, _flash_, _wss_, _ps_, _th_,
                   _tds_, _t3s_, _tes_,
                   _tfs_, _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_,
-                  _ttbs_, _tspr_, _ttip_, _tov_) {
-            var params = $loc.search(),
+                  _ttbs_, _tms_, _tspr_, _ttip_, _tov_) {
+            var params = _$loc_.search(),
                 projection,
                 dim,
                 uplink = {
@@ -531,6 +553,7 @@
 
             $scope = _$scope_;
             $log = _$log_;
+            $loc = _$loc_;
             $cookies = _$cookies_;
             fs = _fs_;
             ks = _ks_;
@@ -551,6 +574,7 @@
             //  just so we can invoke functions on them.
             tps = _tps_;
             tis = _tis_;
+            tms = _tms_;
             tss = _tss_;
             tls = _tls_;
             tts = _tts_;
@@ -561,6 +585,12 @@
             ttip = _ttip_;
             tov = _tov_;
 
+            tms.start({
+                toggleMap: toggleMap,
+                currentMap: currentMap,
+                setMap: setMap
+            });
+
             if (params.intentKey && params.intentAppId && params.intentAppName) {
                 $scope.intentData = {
                     key: params.intentKey,
@@ -577,6 +607,7 @@
             $scope.$on('$destroy', function () {
                 $log.log('OvTopoCtrl is saying Buh-Bye!');
                 tes.stop();
+                tms.stop();
                 ks.unbindKeys();
                 tps.destroyPanels();
                 tds.closeDialog();
diff --git a/web/gui/src/main/webapp/app/view/topo/topoMap.js b/web/gui/src/main/webapp/app/view/topo/topoMap.js
new file mode 100644
index 0000000..407d394
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo/topoMap.js
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Map Module.
+ Defines behavior for loading geographical maps into the map layer.
+ */
+
+(function () {
+    'use strict';
+
+    // injected refs
+    var $log, $loc, fs, flash, wss, tds, delegate;
+
+    // constants
+    var mapRequest = 'mapSelectorRequest';
+
+    // internal state
+    var order, maps, map, mapItems, tintCheck, msgHandlers;
+
+    // === ---------------------------
+    // === Helper functions
+
+
+    // === ---------------------------
+    // === Main API functions
+
+    function openMapSelection() {
+        wss.sendEvent(mapRequest);
+    }
+
+    function closeMapSelection() {
+        tds.closeDialog();
+    }
+
+    function start(d) {
+        delegate = d;
+        wss.bindHandlers(msgHandlers);
+    }
+
+    function stop() {
+        wss.unbindHandlers(msgHandlers);
+    }
+
+    function dOk() {
+        var p = {
+            mapid: map.id,
+            mapscale: map.scale,
+            tint: tintCheck.property('checked') ? 'on' : 'off'
+        };
+        setMap(p);
+        $log.debug('Dialog OK button clicked');
+    }
+
+    function dClose() {
+        $log.debug('Dialog Close button clicked (or Esc pressed)');
+    }
+
+    function selectMap() {
+        map = maps[this.options[this.selectedIndex].value];
+        $log.info('Selected map', map);
+    }
+
+    function createListContent() {
+        var content = tds.createDiv('map-list'),
+            form = content.append('form'),
+            current = currentMap();
+        map = maps[current.mapid];
+        mapItems = form.append('select').on('change', selectMap);
+        order.forEach(function (id) {
+            var m = maps[id];
+            mapItems.append('option')
+                    .attr('value', m.id)
+                    .attr('selected', m.id === current.mapid ? true : null)
+                    .text(m.description);
+        });
+        var p = form.append('p');
+        tintCheck = p.append('input').attr('type', 'checkbox').attr('name', 'tint');
+        if (current.tint == 'on') {
+            tintCheck.attr('checked', 'true');
+        }
+        p.append('span').text('Enable map tint');
+        return content;
+    }
+
+    function handleMapResponse(data) {
+        $log.info('Got response', data);
+        order = data.order;
+        maps = data.maps;
+        tds.openDialog()
+            .setTitle('Select Map')
+            .addContent(createListContent())
+            .addOk(dOk, 'OK')
+            .addCancel(dClose, 'Close')
+            .bindKeys();
+    }
+
+    function toggleMap() {
+        delegate.toggleMap();
+    }
+
+    function currentMap() {
+        return delegate.currentMap();
+    }
+
+    function setMap(map) {
+        delegate.setMap(map);
+    }
+
+    // === -----------------------------------------------------
+    // === MODULE DEFINITION ===
+
+    angular.module('ovTopo')
+    .factory('TopoMapService',
+        ['$log', '$location', 'FnService', 'FlashService', 'WebSocketService',
+            'TopoDialogService',
+
+        function (_$log_, _$loc_, _fs_, _flash_, _wss_, _tds_) {
+            $log = _$log_;
+            $loc = _$loc_;
+            fs = _fs_;
+            flash = _flash_;
+            wss = _wss_;
+            tds = _tds_;
+
+            msgHandlers = {
+                mapSelectorResponse: handleMapResponse
+            };
+
+            return {
+                toggleMap: toggleMap,
+                currentMap: currentMap,
+                setMap: setMap,
+
+                openMapSelection: openMapSelection,
+                closeMapSelection: closeMapSelection,
+                start: start,
+                stop: stop
+            };
+        }]);
+
+}());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoToolbar.js b/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
index 535cd17..f2ea324 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
@@ -49,6 +49,7 @@
         M: { id: 'offline-tog', gid: 'switch', isel: true },
         P: { id: 'ports-tog', gid: 'ports', isel: true },
         B: { id: 'bkgrnd-tog', gid: 'map', isel: false },
+        G: { id: 'bkgrnd-sel', gid: 'filters' },
         S: { id: 'sprite-tog', gid: 'cloud', isel: false },
 
         // TODO: add reset-node-locations button to toolbar
@@ -151,6 +152,7 @@
         addToggle('M');
         addToggle('P', true);
         addToggle('B');
+        addButton('G');
         addToggle('S', true);
     }
 
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index 1a645ad..52f6c2b 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -122,6 +122,7 @@
     <script src="app/view/topo/topoOverlay.js"></script>
     <script src="app/view/topo/topoPanel.js"></script>
     <script src="app/view/topo/topoSelect.js"></script>
+    <script src="app/view/topo/topoMap.js"></script>
     <script src="app/view/topo/topoSprite.js"></script>
     <script src="app/view/topo/topoTraffic.js"></script>
     <script src="app/view/topo/topoTrafficNew.js"></script>