Add MapChart widget

Using TopoJSON [1].

The chart component is build around a convention suggested by d3 author
Mike Bostok [2].

[1] https://github.com/mbostock/topojson
[2] http://bost.ocks.org/mike/chart/

Change-Id: I2b8aefae338f9b24b71d0f2c679993e26dabd6cf
diff --git a/bundles/org.eclipse.rap.addons.chart/js/chart/chart.js b/bundles/org.eclipse.rap.addons.chart/js/chart/chart.js
index e991a9e..b06646b 100644
--- a/bundles/org.eclipse.rap.addons.chart/js/chart/chart.js
+++ b/bundles/org.eclipse.rap.addons.chart/js/chart/chart.js
@@ -69,12 +69,15 @@
     return element;
   },
 
-  notifySelection: function( index, detail ) {
+  notifySelection: function( index, detail, text ) {
     var remoteObject = rap.getRemoteObject( this );
     var params = { "index": index };
     if( arguments.length > 1 ) {
       params.detail = detail;
     }
+    if( arguments.length > 2 ) {
+      params.text = text;
+    }
     remoteObject.notify( "Selection", params );
   },
 
diff --git a/bundles/org.eclipse.rap.addons.chart/js/chart/topojson/topojson-world.js b/bundles/org.eclipse.rap.addons.chart/js/chart/topojson/topojson-world.js
new file mode 100644
index 0000000..b7e4bab
--- /dev/null
+++ b/bundles/org.eclipse.rap.addons.chart/js/chart/topojson/topojson-world.js
@@ -0,0 +1,195 @@
+/*******************************************************************************
+ * Copyright (c) 2016 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+
+/*global topojson:false */
+
+rwt.chart.register( "topojson-world", function( widget ) {
+
+  var _dataPath;
+  var _width;
+  var _height;
+  var _world;
+  var _path;
+  var _showGraticule;
+  var _toolTip;
+  var _scaleFactor = 1;
+  var _center = [ 0, 0 ];
+  var _countries = {};
+  var _colors = [];
+
+  var chart = function( svg, widget ) {
+    if( !_world ) {
+      if( _dataPath ) {
+        createToolTip( widget._element );
+        load( svg );
+      }
+    } else {
+      update( svg );
+    }
+  };
+
+  chart.dataPath = function( path ) {
+    _dataPath = path;
+    return chart;
+  };
+
+  chart.width = function( width ) {
+    _width = width;
+    return chart;
+  };
+
+  chart.height = function( height ) {
+    _height = height;
+    return chart;
+  };
+
+  chart.countries = function( countries ) {
+    _countries = countries;
+    return chart;
+  };
+
+  chart.colors = function( colors ) {
+    _colors = colors;
+    return chart;
+  };
+
+  chart.showGraticule = function( show ) {
+    _showGraticule = show;
+    return chart;
+  };
+
+  chart.scaleFactor = function( factor ) {
+    _scaleFactor = factor;
+    return chart;
+  };
+
+  chart.center = function( center ) {
+    _center = center;
+    return chart;
+  };
+
+  function createToolTip( parentElement ) {
+    if( !_toolTip ) {
+      _toolTip = d3.select( parentElement ).append( "div" ).attr( "class", "tooltip hidden" );
+    }
+  }
+
+  function load( svg ) {
+    d3.json( _dataPath, function( error, world ) {
+      if( error ) {
+        throw error;
+      }
+      _world = world;
+      update( svg );
+    });
+  }
+
+  function update( svg ) {
+    var countries = topojson.feature( _world, _world.objects.countries );
+    calculateColorIndices( countries.features );
+    updateProjection( countries );
+    updateGraticule( svg );
+    svg.selectAll( ".country" )
+      .data( countries.features )
+      .enter()
+      .insert( "path", ".graticule" )
+      .attr( "class", "country" )
+      .attr( "id", function( d ) { return d.id; } )
+      .on( "click", function( d ) {
+        widget.notifySelection( d.id, 0, d.properties.code3 );
+      })
+      .on( "mouseover", function( d ) {
+        if( getCountryData( d ) ) {
+          var text = rwt.util.Encoding.escapeText( getCountryData( d ).label );
+          text = rwt.util.Encoding.replaceNewLines( text, "<br/>" );
+          _toolTip.classed( "hidden", false ).html( text );
+        }
+      })
+      .on( "mouseout", function() {
+        _toolTip.classed( "hidden", true );
+      })
+      .on( "mousemove", function( d ) {
+        if( getCountryData( d ) ) {
+          var mouse = d3.mouse( svg.node() ).map( function( d ) { return parseInt( d ); } );
+          var left = mouse[ 0 ] + 10;
+          var top = mouse[ 1 ] + 15;
+          _toolTip.attr( "style", "left:" + left + "px;top:" + top + "px;" );
+        }
+      });
+    svg.selectAll( ".country" )
+      .attr( "d", _path )
+      .style( "fill", fill );
+  }
+
+  function updateProjection( countries ) {
+    var scale = 150;
+    var offset = [ _width / 2, _height / 2 ];
+    var projection = d3.geo.equirectangular()
+      .scale( scale )
+      .translate( offset )
+      .precision( 0.1 );
+    _path = d3.geo.path().projection( projection );
+    var bounds  = _path.bounds( countries );
+    var hscale  = scale * _width  / ( bounds[ 1 ][ 0 ] - bounds[ 0 ][ 0 ] );
+    var vscale  = scale * _height / ( bounds[ 1 ][ 1 ] - bounds[ 0 ][ 1 ] );
+    scale = ( hscale < vscale ) ? hscale : vscale;
+    projection = d3.geo.equirectangular()
+      .scale( scale * _scaleFactor )
+      .translate( offset )
+      .precision( 0.1 )
+      .center( _center );
+    _path = _path.projection( projection );
+  }
+
+  function updateGraticule( svg ) {
+    if( _showGraticule ) {
+      if( svg.selectAll( ".graticule" ).size() === 0 ) {
+        var graticule = d3.geo.graticule();
+        svg.append( "path" )
+          .datum( graticule )
+          .attr( "class", "graticule" );
+        svg.append( "path" )
+          .datum( graticule.outline )
+          .attr( "class", "graticule outline" );
+      }
+      svg.selectAll( ".graticule" ).attr( "d", _path );
+    } else {
+      svg.selectAll( ".graticule" ).remove();
+    }
+  }
+
+  function fill( d ) {
+    if( getCountryData( d ) ) {
+      return getCountryData( d ).color;
+    }
+    return _colors[ d.col % _colors.length ];
+  }
+
+  function calculateColorIndices( countries ) {
+    var neighbors = topojson.neighbors( _world.objects.countries.geometries );
+    countries.forEach( function( d, i ) {
+      var getCol = function( n ) {
+        return countries[ n ].col;
+      };
+      d.col = d3.max( neighbors[ i ], getCol ) + 1 | 0;
+    });
+  }
+
+  function getCountryData( d ) {
+    var props = d.properties;
+    return _countries[ d.id ] || _countries[ props.code2 ] || _countries[ props.code3 ];
+  }
+
+  widget._scheduleUpdate();
+
+  return chart;
+
+});
diff --git a/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/Chart.java b/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/Chart.java
index bdab119..d48af17 100644
--- a/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/Chart.java
+++ b/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/Chart.java
@@ -77,6 +77,10 @@
           if( detail != null ) {
             event.detail = detail.asInt();
           }
+          JsonValue text = properties.get( "text" );
+          if( text != null ) {
+            event.text = text.asString();
+          }
           notifyListeners( SWT.Selection, event );
         }
       }
diff --git a/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/basic/MapChart.java b/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/basic/MapChart.java
new file mode 100644
index 0000000..e3edc7b
--- /dev/null
+++ b/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/basic/MapChart.java
@@ -0,0 +1,176 @@
+/*******************************************************************************
+ * Copyright (c) 2016 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Ralf Sternberg - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.addons.chart.basic;
+
+import static org.eclipse.rap.addons.chart.internal.ColorUtil.toHtmlString;
+import static org.eclipse.rap.rwt.internal.util.ParamCheck.notNullOrEmpty;
+
+import org.eclipse.rap.addons.chart.Chart;
+import org.eclipse.rap.json.JsonArray;
+import org.eclipse.rap.json.JsonObject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Composite;
+
+
+/**
+ * A basic world map chart widget.
+ *
+ * <dl>
+ * <dt><b>Styles:</b></dt>
+ * <dd>none</dd>
+ * <dt><b>Events:</b></dt>
+ * <dd>Selection</dd>
+ * </dl>
+ */
+@SuppressWarnings( "restriction" )
+public class MapChart extends Chart {
+
+  private static final String PROP_TOPOJSON_JS_URL = "org.eclipse.rap.addons.chart.topojsonJsUrl";
+  private static final String DEF_TOPOJSON_JS_URL
+    = "https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js";
+
+  private boolean showGraticule;
+  private double scaleFactor = 1d;
+  private double longitude;
+  private double latitude;
+
+  /**
+   * Creates a new empty WorldMap chart.
+   *
+   * @param parent a composite control which will be the parent of the new instance (cannot be null)
+   * @param style the style of control to construct
+   * @param path a resource path to registered topology geo data file (cannot be null or empty)
+   *
+   */
+  public MapChart( Composite parent, int style, String path ) {
+    super( parent, style, "topojson-world" );
+    notNullOrEmpty( path, "path" );
+    requireJs( System.getProperty( PROP_TOPOJSON_JS_URL, DEF_TOPOJSON_JS_URL ) );
+    requireJs( registerResource( "chart/topojson/topojson-world.js" ) );
+    requireCss( registerResource( "resources/topojson-world.css" ) );
+    setOption( "dataPath", path );
+  }
+
+  /**
+   * Set array of colors, which will be used to fill the countries with.
+   *
+   * @param colors The array with country fill colors.
+   */
+  public void setColors( RGB[] colors ) {
+    checkWidget();
+    JsonArray json = new JsonArray();
+    for( RGB color : colors ) {
+      json.add( toHtmlString( color ) );
+    }
+    setOption( "colors", json );
+  }
+
+  /**
+   * Set whether graticule should be displayed or not. The default is <code>false</code>.
+   *
+   * @param show <code>true</code> to display graticule.
+   */
+  public void setShowGraticule( boolean show ) {
+    checkWidget();
+    if( show != showGraticule ) {
+      showGraticule = show;
+      setOption( "showGraticule", show );
+    }
+  }
+
+  /**
+   * Returns whether graticule is displayed.
+   *
+   * @return <code>true</code> if graticule is displayed.
+   */
+  public boolean getShowGraticule() {
+    checkWidget();
+    return showGraticule;
+  }
+
+  /**
+   * Set map scale factor. The default is 1.
+   *
+   * @param scaleFactor map scale factor.
+   */
+  public void setScaleFactor( double scaleFactor ) {
+    checkWidget();
+    if( scaleFactor <= 0 ) {
+      SWT.error( SWT.ERROR_INVALID_ARGUMENT );
+    }
+    if( scaleFactor != this.scaleFactor ) {
+      this.scaleFactor = scaleFactor;
+      setOption( "scaleFactor", scaleFactor );
+    }
+  }
+
+  /**
+   * Returns map scale factor.
+   *
+   * @return the scale factor.
+   */
+  public double getScaleFactor() {
+    checkWidget();
+    return scaleFactor;
+  }
+
+  /**
+   * Set map projection's center to the specified location.
+   *
+   * @param longitude location longitude in degrees.
+   * @param latitude location latitude in degrees.
+   */
+  public void setCenter( double longitude, double latitude ) {
+    checkWidget();
+    if( longitude != this.longitude || latitude != this.latitude ) {
+      this.longitude = longitude;
+      this.latitude = latitude;
+      setOption( "center", new JsonArray().add( longitude ).add( latitude ) );
+    }
+  }
+
+  /**
+   * Returns map projection's center as two-element array of longitude and latitude in degrees.
+   *
+   * @return the map projection's center.
+   */
+  public double[] getCenter() {
+    checkWidget();
+    return new double[] { longitude, latitude };
+  }
+
+  /**
+   * Sets the map data items to display. Later changes to this list won't be reflected. To change
+   * the chart data, call this method with a new list of items.
+   *
+   * @param items the map data items to display
+   */
+  public void setItems( MapDataItem... items ) {
+    JsonObject values = new JsonObject();
+    for( MapDataItem item : items ) {
+      values.add( item.getCountry(), toJson( item ) );
+    }
+    setOption( "countries", values );
+  }
+
+  private static JsonObject toJson( MapDataItem item ) {
+    JsonObject json = new JsonObject();
+    if( item.text != null ) {
+      json.add( "label", item.text );
+    }
+    if( item.color != null ) {
+      json.add( "color", toHtmlString( item.color ) );
+    }
+    return json;
+  }
+
+}
diff --git a/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/basic/MapDataItem.java b/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/basic/MapDataItem.java
new file mode 100644
index 0000000..a540a83
--- /dev/null
+++ b/bundles/org.eclipse.rap.addons.chart/src/org/eclipse/rap/addons/chart/basic/MapDataItem.java
@@ -0,0 +1,62 @@
+/*******************************************************************************
+ * Copyright (c) 2016 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Ralf Sternberg - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.addons.chart.basic;
+
+import org.eclipse.swt.graphics.RGB;
+
+public class MapDataItem extends DataItem {
+
+  private final String country;
+
+  /**
+   * Create a new map data item with the given ISO 3166-1 country code.
+   * All numeric/two-letter/three-letter country codes are supported.
+   *
+   * @param country the country of the item
+   */
+  public MapDataItem( String country ) {
+    this( country, null, null );
+  }
+
+  /**
+   * Create a new map data item with the given ISO 3166-1 country code and text.
+   * All numeric/two-letter/three-letter country codes are supported.
+   *
+   * @param country the country of the item
+   * @param text the tooltip text for the item, or <code>null</code> to omit the tooltip
+   */
+  public MapDataItem( String country, String text ) {
+    this( country, text, null );
+  }
+
+  /**
+   * Create a new map data item with the given ISO 3166-1 country code, text, and color.
+   * All numeric/two-letter/three-letter country codes are supported.
+   *
+   * @param country the country of the item
+   * @param text the tooltip text for the item, or <code>null</code> to omit the tooltip
+   * @param color the color of this item, or <code>null</code> to use the default color
+   */
+  public MapDataItem( String country, String text, RGB color ) {
+    super( 0, text, color );
+    this.country = country;
+  }
+
+  /**
+   * Returns the value of this data item.
+   *
+   * @return the item value
+   */
+  public String getCountry() {
+    return country;
+  }
+
+}
diff --git a/bundles/org.eclipse.rap.addons.chart/src/resources/topojson-world.css b/bundles/org.eclipse.rap.addons.chart/src/resources/topojson-world.css
new file mode 100644
index 0000000..1e797f9
--- /dev/null
+++ b/bundles/org.eclipse.rap.addons.chart/src/resources/topojson-world.css
@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * Copyright (c) 2016 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+
+.graticule {
+  fill: none;
+  stroke: #000;
+  stroke-opacity: .1;
+  stroke-width: .5px;
+}
+
+.graticule.outline {
+  stroke: #333;
+  stroke-opacity: 1;
+  stroke-width: 1.5px;
+}
+
+.boundary {
+  fill: none;
+  stroke: #fff;
+  stroke-width: .5px;
+}
+
+.country {
+  fill: #ccc;
+  stroke: #fff;
+  stroke-width: .5px;
+  stroke-linejoin: round;
+}
+
+.country:hover {
+  stroke: #fff;
+  stroke-width: 1.5px;
+  opacity: 0.7;
+}
+
+.tooltip {
+  padding: 10px;
+  background-color: #201F1B;
+  background-image: none;
+  border: none;
+  border-radius: 1px;
+  font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+  color: #e0e0e0;
+  opacity: 0.9;
+  box-shadow: none;
+  text-align: center;
+  position: absolute;
+}
+
+.hidden {
+  display: none;
+}
diff --git a/examples/org.eclipse.rap.addons.chart.demo/src/org/eclipse/rap/addons/chart/demo/MapSnippet.java b/examples/org.eclipse.rap.addons.chart.demo/src/org/eclipse/rap/addons/chart/demo/MapSnippet.java
new file mode 100644
index 0000000..0e5864f
--- /dev/null
+++ b/examples/org.eclipse.rap.addons.chart.demo/src/org/eclipse/rap/addons/chart/demo/MapSnippet.java
@@ -0,0 +1,62 @@
+/*******************************************************************************
+ * Copyright (c) 2016 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Ralf Sternberg - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.addons.chart.demo;
+
+import static org.eclipse.rap.addons.chart.Colors.CATEGORY_10;
+
+import org.eclipse.rap.addons.chart.basic.MapChart;
+import org.eclipse.rap.addons.chart.basic.MapDataItem;
+import org.eclipse.rap.rwt.application.AbstractEntryPoint;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+
+public class MapSnippet extends AbstractEntryPoint {
+
+  private MapChart mapChart;
+
+  @Override
+  public void createContents( Composite parent ) {
+    parent.setLayout( new GridLayout() );
+    createMapChart( parent );
+  }
+
+  private void createMapChart( Composite parent ) {
+    mapChart = new WorldMapChart( parent, SWT.NONE );
+    mapChart.setLayoutData( new GridData( SWT.FILL, SWT.FILL, true, true ) );
+    //mapChart.setShowGraticule( true );
+    mapChart.setScaleFactor( 5 );
+    mapChart.setCenter( 23.3219, 42.6977 );
+    mapChart.addListener( SWT.Selection, new Listener() {
+      @Override
+      public void handleEvent( Event event ) {
+        System.out.println( "Selected country #" + event.index + "," + event.text );
+      }
+    } );
+    //mapChart.setColors( Colors.CATEGORY_10 );
+    mapChart.setItems( createItems() );
+  }
+
+  private static MapDataItem[] createItems() {
+    return new MapDataItem[] {
+      new MapDataItem( "100", "Bulgaria\nSofia", CATEGORY_10[ 0 ] ),
+      new MapDataItem( "DEU", "Item 2", CATEGORY_10[ 1 ] ),
+      new MapDataItem( "ES", "Item 3", CATEGORY_10[ 2 ] ),
+      new MapDataItem( "AUT", "Item 4", CATEGORY_10[ 3 ] ),
+      new MapDataItem( "ITA", "Item 5", CATEGORY_10[ 4 ] )
+    };
+  }
+
+}
diff --git a/examples/org.eclipse.rap.addons.chart.demo/src/org/eclipse/rap/addons/chart/demo/WorldMapChart.java b/examples/org.eclipse.rap.addons.chart.demo/src/org/eclipse/rap/addons/chart/demo/WorldMapChart.java
new file mode 100644
index 0000000..e7f61d6
--- /dev/null
+++ b/examples/org.eclipse.rap.addons.chart.demo/src/org/eclipse/rap/addons/chart/demo/WorldMapChart.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2016 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Ralf Sternberg - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.addons.chart.demo;
+
+import static org.eclipse.rap.rwt.RWT.getResourceManager;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.eclipse.rap.addons.chart.basic.MapChart;
+import org.eclipse.rap.rwt.service.ResourceLoader;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.swt.widgets.Composite;
+
+
+public class WorldMapChart extends MapChart {
+
+  public WorldMapChart( Composite parent, int style ) {
+    super( parent, style, registerGeoData( "resources/world-110m.json" ) );
+  }
+
+  private static String registerGeoData( String path ) {
+    ResourceManager resourceManager = getResourceManager();
+    if( !resourceManager.isRegistered( path ) ) {
+      try (InputStream inputStream = getResourceLoader().getResourceAsStream( path )) {
+        resourceManager.register( path, inputStream );
+      } catch( Exception exception ) {
+        throw new RuntimeException( "Failed to register resource " + path, exception );
+      }
+    }
+    return resourceManager.getLocation( path );
+  }
+
+  private static ResourceLoader getResourceLoader() {
+    final ClassLoader classLoader = WorldMapChart.class.getClassLoader();
+    return new ResourceLoader() {
+      @Override
+      public InputStream getResourceAsStream( String resourceName ) throws IOException {
+        return classLoader.getResourceAsStream( resourceName );
+      }
+    };
+  }
+
+}
diff --git a/tests/org.eclipse.rap.addons.chart.test/src/org/eclipse/rap/addons/chart/basic/MapChart_Test.java b/tests/org.eclipse.rap.addons.chart.test/src/org/eclipse/rap/addons/chart/basic/MapChart_Test.java
new file mode 100644
index 0000000..2d2afbe
--- /dev/null
+++ b/tests/org.eclipse.rap.addons.chart.test/src/org/eclipse/rap/addons/chart/basic/MapChart_Test.java
@@ -0,0 +1,218 @@
+/*******************************************************************************
+ * Copyright (c) 2016 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Ralf Sternberg - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.addons.chart.basic;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.rap.json.JsonArray;
+import org.eclipse.rap.json.JsonObject;
+import org.eclipse.rap.json.JsonValue;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.client.Client;
+import org.eclipse.rap.rwt.client.service.JavaScriptLoader;
+import org.eclipse.rap.rwt.internal.remote.ConnectionImpl;
+import org.eclipse.rap.rwt.remote.Connection;
+import org.eclipse.rap.rwt.remote.RemoteObject;
+import org.eclipse.rap.rwt.testfixture.TestContext;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+@SuppressWarnings( { "restriction", "deprecation" } )
+public class MapChart_Test {
+
+  private static final String PROP_TOPOJSON_JS_URL = "org.eclipse.rap.addons.chart.topojsonJsUrl";
+  private static final String DEF_TOPOJSON_JS_URL
+    = "https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js";
+
+  private Display display;
+  private Shell shell;
+  private RemoteObject remoteObject;
+  private Connection connection;
+  private MapChart chart;
+
+  @Rule
+  public TestContext context = new TestContext();
+
+  @Before
+  public void setUp() {
+    display = new Display();
+    shell = new Shell( display );
+    remoteObject = mock( RemoteObject.class );
+    connection = fakeConnection( remoteObject );
+    chart = new MapChart( shell, SWT.NONE, "resources/world-110m.json" );
+  }
+
+  @Test
+  public void testCreate_requiresTopojsonJS() {
+    JavaScriptLoader loader = mock( JavaScriptLoader.class );
+    fakeLoader( loader );
+
+    new MapChart( shell, SWT.NONE, "resources/world-110m.json" );
+
+    verify( loader ).require( DEF_TOPOJSON_JS_URL );
+  }
+
+  @Test
+  public void testCreate_requiresTopojsonJS_fromSystemProperty() {
+    JavaScriptLoader loader = mock( JavaScriptLoader.class );
+    fakeLoader( loader );
+
+    System.setProperty( PROP_TOPOJSON_JS_URL, "custom://url" );
+    new MapChart( shell, SWT.NONE, "resources/world-110m.json" );
+    System.clearProperty( PROP_TOPOJSON_JS_URL );
+
+    verify( loader, never() ).require( DEF_TOPOJSON_JS_URL );
+    verify( loader ).require( "custom://url" );
+  }
+
+  @Test
+  public void testCreate_registeresJavaScriptResource() {
+    assertTrue( RWT.getResourceManager().isRegistered( "chart/topojson/topojson-world.js" ) );
+  }
+
+
+  @Test
+  public void testCreate_registeresCssResource() {
+    assertTrue( RWT.getResourceManager().isRegistered( "resources/topojson-world.css" ) );
+  }
+
+  @Test
+  public void testCreate_createsRemoteObject() {
+    verify( connection ).createRemoteObject( eq( "rwt.chart.Chart" ) );
+  }
+
+  @Test
+  public void testCreate_setsRenderer() {
+    verify( remoteObject ).set( "renderer", "topojson-world" );
+  }
+
+  @Test
+  public void testCreate_setsDataPath() {
+    JsonObject expected = new JsonObject().add( "dataPath", "resources/world-110m.json" );
+    verify( remoteObject ).call( "setOptions", expected );
+  }
+
+  @Test
+  public void testGetGraticule_defaultsToFalse() {
+    assertFalse( chart.getShowGraticule() );
+  }
+
+  @Test
+  public void testSetGraticule_changesValue() {
+    chart.setShowGraticule( true );
+
+    assertTrue( chart.getShowGraticule() );
+  }
+
+  @Test
+  public void testSetGraticule_isRendered() {
+    chart.setShowGraticule( true );
+
+    verify( remoteObject ).call( "setOptions", new JsonObject().add( "showGraticule", true ) );
+  }
+
+  @Test
+  public void testGetScaleFactor_defaultsToOne() {
+    assertEquals( 1d, chart.getScaleFactor(), 0d );
+  }
+
+  @Test
+  public void testSetScaleFactor_changesValue() {
+    chart.setScaleFactor( 1.1 );
+
+    assertEquals( 1.1, chart.getScaleFactor(), 0d );
+  }
+
+  @Test
+  public void testSetScaleFactor_isRendered() {
+    chart.setScaleFactor( 1.1 );
+
+    verify( remoteObject ).call( "setOptions", new JsonObject().add( "scaleFactor", 1.1 ) );
+  }
+
+  @Test
+  public void testGetCenter_defaultsToZero() {
+    assertArrayEquals( new double[] { 0d, 0d }, chart.getCenter(), 0d );
+  }
+
+  @Test
+  public void testSetCenter_changesValue() {
+    chart.setCenter( 1, 2 );
+
+    assertArrayEquals( new double[] { 1d, 2d }, chart.getCenter(), 0d );
+  }
+
+  @Test
+  public void testSetCenter_isRendered() {
+    chart.setCenter( 1, 2 );
+
+    JsonArray center = new JsonArray().add( 1 ).add( 2 );
+    verify( remoteObject ).call( "setOptions", new JsonObject().add( "center", center ) );
+  }
+
+  @Test
+  public void testSetColors_byArray_isRendered() {
+    RGB[] colors = {
+      new RGB( 0x1f, 0x77, 0xb4 ),
+      new RGB( 0xff, 0x7f, 0x0e ),
+      new RGB( 0x2c, 0xa0, 0x2c )
+    };
+
+    chart.setColors( colors );
+
+    JsonArray expectedColors = new JsonArray()
+        .add( "#1f77b4" )
+        .add( "#ff7f0e" )
+        .add( "#2ca02c" );
+    verify( remoteObject ).call( "setOptions", new JsonObject().add( "colors", expectedColors ) );
+  }
+
+  @Test
+  public void testSetItems() {
+    chart.setItems( new MapDataItem( "BGR", "foo" ),
+                    new MapDataItem( "DEU", "bar", new RGB( 0x1f, 0x77, 0xb4 ) ) );
+
+    String expected = "{\"countries\": {"
+                    + "  \"BGR\": { \"label\": \"foo\" },"
+                    + "  \"DEU\": { \"label\": \"bar\", \"color\": \"#1f77b4\" }"
+                    + "}}";
+    verify( remoteObject ).call( "setOptions", JsonValue.readFrom( expected ).asObject() );
+  }
+
+  private void fakeLoader( JavaScriptLoader loader ) {
+    Client client = mock( Client.class );
+    when( client.getService( JavaScriptLoader.class ) ).thenReturn( loader );
+    context.replaceClient( client );
+  }
+
+  private Connection fakeConnection( RemoteObject remoteObject ) {
+    ConnectionImpl connection = mock( ConnectionImpl.class );
+    when( connection.createRemoteObject( anyString() ) ).thenReturn( remoteObject );
+    when( connection.createServiceObject( anyString() ) ).thenReturn( mock( RemoteObject.class ) );
+    context.replaceConnection( connection );
+    return connection;
+  }
+
+}