Show:

File: platform/plugins/Places/web/js/tools/globe.js

(function (Q, $, window, document, undefined) {

var Places = Q.Places;

/**
 * Places Tools
 * @module Places-tools
 */

/**
 * Displays a globe, using planetary.js
 * @class Places globe
 * @constructor
 * @param {Object} [options] used to pass options
 * @param {String} [options.countryCode='US'] the initial country to rotate to and highlight
 * @param {Array} [options.highlight={US:true}] pairs of {countryCode: color},
 *   if color is true then state.colors.highlight is used.
 *   This is modified by the default handler for beforeRotateToCountry added by this tool.
 * @param {Number} [options.radius=0.9] The radius of the globe, as a fraction of Math.min(canvas.width, canvas.height) / 2.
 * @param {Number} [options.durations] The duration of rotation animation (it all rhymes baby)
 * @param {Object} [options.colors] colors for the planet
 * @param {String} [options.colors.oceans='#2a357d'] the color of the ocean
 * @param {String} [options.colors.land='#389631'] the color of the land
 * @param {String} [options.colors.borders='#008000'] the color of the borders
 * @param {Object} [options.pings] default options for any pings added with addPing
 * @param {Object} [options.pings.duration=2000] default duration of any ping animation
 * @param {Object} [options.pings.size=10] default size of any ping animation
 * @param {Object} [options.pings.color='white'] default color of any ping animation
 * @param {Object} [options.shadow] shadow effect configuration
 * @param {String} [options.shadow.src="{{Q}}/img/shadow3d.png"] src , path to image
 * @param {Number} [options.shadow.stretch=1.5] stretch
 * @param {Number} [options.shadow.dip=0.25] dip
 * @param {Number} [options.shadow.opacity=0.5] opacity
 * @param {Q.Event} [options.onReady] this event occurs when the globe is ready
 * @param {Q.Event} [options.onSelect] this event occurs when the user has selected a country or a place on the globe. It is passed (latitude, longitude, countryCode)
 * @param {Q.Event} [options.beforeRotate] this event occurs right before the globe is about to rotate to some location
 * @param {Q.Event} [options.beforeRotateToCountry] this event occurs right before the globe is about to rotate to some country
 */
Q.Tool.define("Places/globe", function _Places_globe(options) {
	var tool = this;
	var state = tool.state;
	var $te = $(tool.element);
	
	var p = Q.pipe(['scripts', 'countries'], function _proceed() {
		tool.$canvas = $('<canvas />').attr({
			width: $te.outerWidth(),
			height: $te.outerHeight()
		}).appendTo($te)
		.on(Q.Pointer.fastclick, tool, function(event) {
			var ll = tool.getCoordinates(event);
			tool.geocoder.geocode(
				{'location': { lat: ll.latitude, lng: ll.longitude }},
				function(results, status) {
					if (status === google.maps.GeocoderStatus.OK && results[0]) {
						var countryCode = _getComponent(results[0], 'country');
						tool.rotateToCountry(countryCode);
					} else {
						tool.rotateTo(ll.latitude, ll.longitude);
					}
					Q.handle(state.onSelect, [ll.latitude, ll.longitude, countryCode]);
				}
			);
		});
		
		if (!state.radius) {
			state.radius = 0.9;
		}
		
		var globe = tool.globe = planetaryjs.planet();
		
		globe.onInit(function () {
			Q.handle(state.onReady, tool);
		});
		
		// The `earth` plugin draws the oceans and the land; it's actually
		// a combination of several separate built-in plugins.
		//
		// Note that we're loading a special TopoJSON file
		// (world-110m-withlakes.json) so we can render lakes.
		globe.loadPlugin(planetaryjs.plugins.earth({
			topojson: { file:   Q.url('Q/plugins/Places/data/world-110m-withlakes.json') },
			oceans:   { fill:   state.colors.oceans },
			land:	 { fill:   state.colors.land },
			borders:  { stroke: state.colors.borders }
		}));
		
		// Load our custom `lakes` plugin to draw lakes
		globe.loadPlugin(_lakes({
			fill: state.colors.oceans
		}));
		
		// Load our custom `lakes` plugin to highlight countries
		globe.loadPlugin(_highlight({
			tool: tool
		}));
		
		// The zoom and drag plugins enable
		// manipulating the globe with the mouse.
		var half = Math.min(tool.$canvas.width(), tool.$canvas.height()) / 2;
		var radius = half * state.radius;
		globe.loadPlugin(planetaryjs.plugins.zoom({
			scaleExtent: [radius, 20 * state.radius]
		}));
		globe.loadPlugin(planetaryjs.plugins.drag({}));
		
		// Set up the globe's initial scale, offset, and rotation.
		globe.projection
			.scale(radius)
			.translate([half, half])
			.rotate([0, -10, 0]);
		
		globe.loadPlugin(planetaryjs.plugins.pings());
		
		if (state.shadow && state.shadow.src) {
			var shadow = $('<img />').addClass('Places_globe_shadow')
				.attr('src', Q.url(state.shadow.src));
			shadow.css('display', 'none').prependTo($te).load(function () {
				var $this = $(this);
				var w = h = radius * 2;
				var width = w * state.shadow.stretch;
				var height = Math.min($this.height() * width / $this.width(), h/2);
				var toSet = {
					'position': 'absolute',
					'left': ($te.outerWidth() - width)/2+'px',
					'top': $te.outerHeight() - height * (1-state.shadow.dip)+'px',
					'width': width+'px',
					'height': height+'px',
					'opacity': state.shadow.opacity,
					'display': '',
					'padding': '0px',
					'background': 'none',
					'border': '0px',
					'outline': '0px',
					'z-index': 1
				};
				var i, l, props = Object.keys(toSet);
				$this.css(toSet);
			});
			var $placeholder = $('<div class="Places_globe_placeholder" />').css({
				width: tool.$canvas.outerWidth(),
				height: tool.$canvas.outerHeight()
			}).insertAfter(tool.$canvas);
			tool.$canvas.css({
				'position': 'absolute',
				'z-index': 2
			});
			if ($te.css('position') === 'static') {
				$te.css('position', 'relative');
			}
		}
		
		$te.on('touchmove', function (e) {
			e.preventDefault();
		});
		
		tool.refresh();
	});
	
	Q.addScript([
		'Q/plugins/Places/js/lib/d3.js',
		'Q/plugins/Places/js/lib/topojson.js',
		'Q/plugins/Places/js/lib/planetaryjs.js'
	], p.fill('scripts'));
	
	Places.loadCountries(p.fill('countries'));
},

{ // default options here
	countryCode: 'US',
	colors: {
		oceans: '#2a357d',
		land: '#389631',
		borders: '#008000',
		highlight: '#ff0000'
	},
	highlight: {'US':true},
	radius: null,
	duration: 1000,
	pings: {
		duration: 2000,
		size: 10,
		color: 'white'
	},
	shadow: {
		src: "{{Q}}/img/shadow3d.png",
		stretch: 1.2,
		dip: 0.25,
		opacity: 0.5
	},
	onReady: new Q.Event(),
	onRefresh: new Q.Event(),
	beforeRotate: new Q.Event(),
	beforeRotateToCountry: new Q.Event(function (countryCode) {
		var h = this.state.highlight = {};
		h[countryCode] = true;
	}, "Place/globe"),
	onRotateEnded: new Q.Event()
},

{ // methods go here
	
	refresh: function _Places_globe_refresh () {
		var tool = this;
		var state = tool.state;
		var $te = $(tool.element);
		Places.loadGoogleMaps(function () {
			tool.geocoder = new google.maps.Geocoder;
			tool.globe.draw(tool.$canvas[0]);
			var waitForTopoJsonLoad = setInterval(_a, 50);
			function _a() {
				if (!Q.getObject('globe.plugins.topojson.world', tool)) return;
				tool.rotateToCountry(state.countryCode);
				clearInterval(waitForTopoJsonLoad);
				Q.handle(state.onRefresh, tool);
			}
			_a();
		});
	},
	
	/**
	 * Obtain the coordinates of a country's cener
	 * @param {String} countryCode
	 * @return {Object|null} An object with properties "latitude" and "longitude"
	 */
	countryCenter: function Places_globe_countryCenter (countryCode) {
		var feature = _getFeature(tool.globe, countryCode);
		if (!feature) {
			return false;
		}
		var p = d3.geo.centroid(feature);
		return { latitude: p[0], longitude: p[1] };
	},
	
	/**
	 * Rotate the globe to center around a location
	 * @param {Number} latitude
	 * @param {Number} longitude
	 * @param {Number} [duration=state.duration] number of milliseconds for the animation to take
	 */
	rotateTo: Q.preventRecursion('Places/globe rotateTo', 
	function Places_globe_rotateTo (latitude, longitude, duration, callback) {
		var tool = this;
		duration = duration || this.state.duration;
		Q.handle(tool.state.beforeRotate, tool, [latitude, longitude, duration]);
		d3.transition()
			.duration(duration)
			.tween('rotate', function() {
				var projection = tool.globe.projection;
				var r = d3.interpolate(projection.rotate(), [-longitude, -latitude]);
				tool.center = {
					latitude: latitude,
					longitude: longitude
				};
				return function(t) {
					projection.rotate(r(t));
					callback && callback.apply(this, arguments);
				};
			})
			.transition();
	}),
		
	/**
	 * Rotate the globe to center around a country
	 * @param {String} countryCode which is described in ISO-3166-1 alpha-2 code
	 * @param {Number} duration number of milliseconds for the animation to take
	 * @return {Boolean} whether the country was found on the globe, and the rotation started
	 */
	rotateToCountry: Q.preventRecursion('Q/globe rotateToCountry', 
	function Places_globe_rotateToCountry (countryCode, duration) {
		var tool = this;
		var feature = _getFeature(tool.globe, countryCode);
		if (!feature) {
			return false;
		}
		var c = tool.$canvas[0].getContext("2d");
		var projection = tool.globe.projection;
		var path = d3.geo.path().projection(projection).context(c);
		// var tj = tool.globe.plugins.topojson;
		// var land = topojson.feature(tj.world, tj.world.objects.land);
		// var borders = topojson.mesh(
		// 	tj.world, tj.world.objects.countries,
		// 	function(a, b) { return a !== b; }
		// );
		var p = d3.geo.centroid(feature);
		Q.handle(tool.state.beforeRotateToCountry, tool, [countryCode, p[1], p[0], duration]);
		var transition = tool.rotateTo(p[1], p[0], duration, function () {
			c.fillStyle = tool.state.colors.highlight;
			c.beginPath();
			path(feature);
			c.fill();
		});
		return true;
	}),
	
	/**
	 * Obtain latitude and longitude from a pointer event
	 * @param {Event} event some pointer event
	 * @return {Object} object with properties "latitude", "longitude"
	 */
	getCoordinates: function Places_globe_getCoordinates(event) {
		var tool = this;
		var offset = $(event.target).offset();
		var x = Q.Pointer.getX(event) - offset.left;
		var y = Q.Pointer.getY(event) - offset.top;
		var coordinates = tool.globe.projection.invert([x, y]);
		return {
			latitude: coordinates[1],
			longitude: coordinates[0]
		}
	},
	
	/**
	 * Adds a ping to start animating immediately
	 * @param {Number} latitude The latitude of the center of the ping
	 * @param {Number} longitude The longitude of the center of the ping
	 * @param {Number} [duration=state.pings.duration] Number of milliseconds for the ping growing animation
	 * @param {Number} [size=state.pings.size] Maximum angle, in degrees, for the ping circle to grow to
	 * @param {Number} [size=state.pings.color] Color of the ping circle
	 */
	addPing: function (latitude, longitude, duration, size, color) {
		var state = this.state;
		var globe = this.globe;
		if (!globe.plugins.pings) return;
		globe.plugins.pings.add(longitude, latitude, { 
			color: color || state.pings.color, 
			ttl: duration || state.pings.duration, 
			angle: size || state.pings.size
		});
	}
	
});

/**
 * Looking for a desired type in the results and getting component using typeName
 * @param {Object} results
 * @param {String} desiredType, for example 'country'
 * @param {?String} typeName, for example 'long_name'. If it doesn't set it is equal to 'short_name'
 * @returns {*}
 */
function _getComponent(result, desiredType, typeName) {
	typeName = typeName || 'short_name';
	var address_components = result.address_components;
	for (var i = 0; i < address_components.length; i++) {
		var shortname = address_components[i].short_name;
		var type = address_components[i].types;
		if (type.indexOf(desiredType) != -1) {
			var c = address_components[i][typeName];
			return (c == null || !c.trim().length) ? shortname : c;
		}
	}
}

// This plugin takes lake data from the special
// TopoJSON we're loading and draws them on the map.
function _lakes(options) {
	options = options || {};
	var lakes = null;
	return function(planet) {
		planet.onInit(function() {
			/**
			 * We can access the data loaded from the TopoJSON plugin on its namespace on `planet.plugins`.
			 * We're loading a custom TopoJSON file with an object called "ne_110m_lakes".
			 */
			var world = planet.plugins.topojson.world;
			lakes = topojson.feature(world, world.objects.ne_110m_lakes);
		});

		planet.onDraw(function() {
			planet.withSavedContext(function(context) {
				context.beginPath();
				planet.path.context(context)(lakes);
				context.fillStyle = options.fill || 'black';
				context.fill();
			});
		});
	};
};

// This plugin highlights countries
function _highlight(options) {
	options = options || {};
	var tool = options.tool;
	return function(planet) {
		planet.onDraw(function() {
			planet.withSavedContext(function(context) {
				Q.each(tool.state.highlight, function (countryCode) {
					var feature = _getFeature(tool.globe, countryCode);
					if (!feature) {
						return;
					}
					var c = tool.$canvas[0].getContext("2d");
					var projection = tool.globe.projection;
					var path = d3.geo.path().projection(projection).context(c);
					var color = tool.state.highlight[countryCode];
					color = typeof color === 'string' ? color : tool.state.colors.highlight;
					var c = tool.$canvas[0].getContext("2d");
					c.fillStyle = color;
					c.beginPath();
					path(feature);
					c.fill();
				});
			});
		});
	};
};

// Gets the country's feature, if any
function _getFeature(planet, countryCode) {
	var countryName, lookup, tj, countries, features, feature;
	var parts = Places.countriesByCode[countryCode];
	if (!parts) {
		return parts;
	}
	countryName = parts[0];
	lookup = Places.countryLookupByCode[countryCode];
	if (tj = planet.plugins.topojson) {
		if (!tj.world) {
			return null;
		}
		countries = tj.world.objects.countries;
		features = topojson.feature(tj.world, countries).features;
		feature = null;
		Q.each(features, function () {
			if (this.id == lookup) {
				feature = this;
			}
		});
	}
	return feature;
}

})(Q, jQuery, window, document);