PageRenderTime 27ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/angular-globe.js

https://gitlab.com/sgruhier/angular-globe
JavaScript | 351 lines | 308 code | 42 blank | 1 comment | 37 complexity | b91532be7ace9abac2145d65d9ebca92 MD5 | raw file
  1. (function() {
  2. 'use strict';
  3. angular
  4. .module('madvas.angular-globe', [])
  5. .directive('m2sGlobe', m2sGlobe);
  6. m2sGlobe.$inject = [];
  7. /* @ngInject */
  8. function m2sGlobe() {
  9. var defaultPointFill = '#8D6E63';
  10. var defaultLandTop = '#dadac4';
  11. var defaultLandBottom = '#737368';
  12. var defaultWater = '#FFF';
  13. var defaultPointStroke = '#000';
  14. var defaultEase = 'cubic';
  15. var defaultPointStrokeWidth = 0;
  16. var defaultPointRadius = 5;
  17. var defaultGlobeSize = 400;
  18. var defaultAnimDuration = 250;
  19. var globeSize;
  20. var svg;
  21. var path;
  22. var projection;
  23. var points;
  24. var landGroup;
  25. var water;
  26. var graticule;
  27. var shadow;
  28. var landBottom;
  29. var landTop;
  30. var scope;
  31. var raisedLand;
  32. var rotateStart = new Date();
  33. return {
  34. link : link,
  35. restrict : 'EA',
  36. scope : {
  37. points : '=',
  38. mapData : '=',
  39. mapType : '@',
  40. size : '&',
  41. pointStroke : '&',
  42. pointFill : '&',
  43. pointClass : '&',
  44. pointId : '&',
  45. pointLat : '&',
  46. pointLng : '&',
  47. pointRadius : '&',
  48. pointStrokeWidth : '&',
  49. graticule : '&',
  50. draggable : '&',
  51. sensitivity : '&',
  52. clickable : '&',
  53. pointClick : '&',
  54. raisedLand : '&',
  55. rotate : '&',
  56. rotateSpeed : '&',
  57. rotateAngle : '&',
  58. enterAnimDuration : '&',
  59. exitAnimDuration : '&',
  60. enterAnimDelay : '&',
  61. exitAnimDelay : '&',
  62. enterAnimEase : '&',
  63. exitAnimEase : '&'
  64. }
  65. };
  66. function link(_scope_, el) {
  67. scope = _scope_;
  68. raisedLand = scope.raisedLand();
  69. globeSize = scope.size() || el[0].clientWidth || defaultGlobeSize;
  70. d3.select(window).on('resize', resize.bind(null, el));
  71. projection = d3.geo.orthographic()
  72. .translate([globeSize / 2, globeSize / 2])
  73. .center([0, 0])
  74. .precision(0.5);
  75. svg = d3.select(el[0]).append('svg')
  76. .attr({
  77. 'class' : 'm2s-globe',
  78. width : globeSize,
  79. height : globeSize
  80. });
  81. path = d3.geo.path()
  82. .projection(projection);
  83. projection.scale(globeSize / 2.3).clipAngle(90);
  84. landGroup = svg.append('g')
  85. .attr('class', 'm2s-globe-land-group');
  86. water = landGroup.append('path')
  87. .datum({type : 'Sphere'})
  88. .attr('class', 'm2s-globe-water')
  89. .attr('fill', defaultWater)
  90. .attr('d', path);
  91. if (scope.graticule() !== false) {
  92. graticule = landGroup.selectAll('path.m2s-globe-land-group')
  93. .data(d3.geo.graticule().lines())
  94. .enter().append('path')
  95. .attr({
  96. fill : 'none',
  97. stroke : defaultLandTop,
  98. 'stroke-opacity' : 0.5,
  99. 'class' : 'm2s-globe-graticule'
  100. });
  101. }
  102. scope.$watch('mapData', mapDataChanged);
  103. scope.$watch('points', pointsChanged, true);
  104. scope.$watch('draggable', draggableChanged);
  105. scope.$watch('sensitivity', draggableChanged);
  106. scope.$watch('clickable', initClickable);
  107. scope.$watch('rotate', rotateChange);
  108. scope.$watch('rotateAngle', rotateAngleChange);
  109. }
  110. function mapDataChanged(newValue) {
  111. if (!newValue && mapData) {
  112. var type = scope.mapType || 'land';
  113. newValue = topojson.feature(mapData, mapData.objects[type]);
  114. }
  115. if (newValue) {
  116. d3.selectAll('.m2s-globe-land').remove();
  117. if (raisedLand !== false) {
  118. shadow = landGroup.append('path')
  119. .datum(newValue)
  120. .attr('class', 'm2s-globe-land m2s-globe-shadow')
  121. .style({
  122. '-webkit-filter' : 'blur(6px)',
  123. 'filter' : 'blur(6px)',
  124. 'opacity' : 0.4
  125. });
  126. landBottom = landGroup.append('path')
  127. .datum(newValue)
  128. .attr('fill', defaultLandBottom)
  129. .attr('class', 'm2s-globe-land m2s-globe-land-bottom');
  130. }
  131. landTop = landGroup.append('path')
  132. .datum(newValue)
  133. .attr('fill', defaultLandTop)
  134. .attr('class', 'm2s-globe-land m2s-globe-land-top');
  135. updateGlobe();
  136. } else {
  137. d3.selectAll('.m2s-globe-land').remove();
  138. }
  139. }
  140. function pointsChanged(newData) {
  141. newData = newData || [];
  142. var pointsData = [];
  143. angular.forEach(newData, function(pointGroup) {
  144. if (pointGroup.values && pointGroup.values.length) {
  145. pointsData = pointsData.concat(pointGroup.values.map(createPointFeature));
  146. }
  147. });
  148. points = svg.selectAll('.m2s-globe-points').data(pointsData);
  149. points.enter()
  150. .append('path')
  151. .attr({
  152. opacity : function(d, i) {
  153. return getTransitionDelay('enter', d, i) > 0 ? 0 : 1;
  154. }
  155. })
  156. .transition()
  157. .ease(scope.enterAnimEase() || defaultEase)
  158. .delay(function(d, i) {
  159. return getTransitionDelay('enter', d, i) || 0;
  160. })
  161. .duration(function(d, i) {
  162. return getTransitionDuration('enter', d, i) || defaultAnimDuration;
  163. })
  164. .attr('opacity', 1);
  165. points.attr({
  166. fill : dataFn(scope.pointFill, defaultPointFill),
  167. stroke : dataFn(scope.pointStroke, defaultPointStroke),
  168. 'stroke-width' : dataFn(scope.pointStrokeWidth, defaultPointStrokeWidth),
  169. }).attr('id', function(d) {
  170. return d.id;
  171. }).attr('class', function(d) {
  172. return dataFn(scope.pointClass)(d) + ' m2s-globe-points';
  173. });
  174. initClickable(scope.clickable);
  175. points.exit()
  176. .transition()
  177. .ease(scope.exitAnimEase() || defaultEase)
  178. .delay(function(d, i) {
  179. return getTransitionDelay('exit', d, i) || 0;
  180. })
  181. .duration(function(d, i) {
  182. return getTransitionDuration('exit', d, i) || defaultAnimDuration;
  183. })
  184. .attr('opacity', 0)
  185. .remove();
  186. updateGlobe();
  187. }
  188. function dataFn(func, defaultVal) {
  189. return function(d) {
  190. return func({d : d.properties}) || defaultVal;
  191. };
  192. }
  193. function updateGlobe() {
  194. projection.scale(globeSize / 2.3).clipAngle(90);
  195. if (scope.graticule() !== false) {
  196. graticule.attr('d', path);
  197. }
  198. if (raisedLand !== false) {
  199. if (shadow) {
  200. shadow.attr('d', path);
  201. }
  202. projection.scale(globeSize / 2.2).clipAngle(106.3);
  203. if (landBottom) {
  204. landBottom.attr('d', path);
  205. }
  206. projection.scale(globeSize / 2.2).clipAngle(90);
  207. }
  208. if (landTop) {
  209. landTop.attr('d', path);
  210. }
  211. svg.selectAll('.m2s-globe-points').attr('d', function(d) {
  212. return path.pointRadius(dataFn(scope.pointRadius, defaultPointRadius)(d))(d);
  213. });
  214. }
  215. function createPointFeature(point) {
  216. return {
  217. type : 'Feature',
  218. id : dataFn(scope.pointId, uniqueId())({properties : point}),
  219. properties : point,
  220. geometry : {
  221. type : 'Point',
  222. coordinates : [
  223. dataFn(scope.pointLng)({properties : point}),
  224. dataFn(scope.pointLat)({properties : point})
  225. ]
  226. }
  227. };
  228. }
  229. function draggableChanged() {
  230. var draggable = scope.draggable();
  231. var sens = scope.sensitivity() || 0.25;
  232. if (draggable) {
  233. landGroup.call(d3.behavior.drag()
  234. .origin(function() {
  235. var r = projection.rotate();
  236. return {x : r[0] / sens, y : -r[1] / sens};
  237. })
  238. .on('drag', function() {
  239. var rotate = projection.rotate();
  240. projection.rotate([d3.event.x * sens, -d3.event.y * sens, rotate[2]]);
  241. updateGlobe();
  242. }));
  243. } else {
  244. landGroup.on('dragstart', null)
  245. .on('drag', null)
  246. .on('dragend', null);
  247. }
  248. }
  249. function getTransitionDelay(type, d, i) {
  250. return scope[type + 'AnimDelay']({d : d, i : i}) || 0;
  251. }
  252. function getTransitionDuration(type, d, i) {
  253. return scope[type + 'AnimDuration']({d : d, i : i}) || 0;
  254. }
  255. function initClickable(clickable) {
  256. if (clickable() === false) {
  257. points.style('cursor', 'normal');
  258. points.on('click', null);
  259. } else {
  260. points.style('cursor', 'pointer');
  261. points.on('click', pointClicked);
  262. }
  263. }
  264. function pointClicked(d) {
  265. var coords = [dataFn(scope.pointLng)(d), dataFn(scope.pointLat)(d)];
  266. var el = d3.select('#' + d.id);
  267. scope.$apply(function() {
  268. scope.pointClick({d : d.properties, c : projection(coords), el : el});
  269. });
  270. }
  271. function rotateChange() {
  272. if (scope.rotate()) {
  273. d3.timer(function() {
  274. var speed = scope.rotateSpeed() || scope.rotateSpeed() === 0 ? scope.rotateSpeed() : 1;
  275. projection.rotate([speed / 100 * (Date.now() - rotateStart), -15]);
  276. updateGlobe();
  277. return !scope.rotate();
  278. });
  279. }
  280. }
  281. function rotateAngleChange(newValue) {
  282. newValue = newValue();
  283. if (newValue && newValue.length === 2) {
  284. projection.rotate([newValue[0], newValue[1]]);
  285. }
  286. }
  287. function resize(el) {
  288. if (!scope.size()) {
  289. globeSize = el[0].clientWidth || defaultGlobeSize;
  290. svg.attr({
  291. width : globeSize,
  292. height : globeSize
  293. });
  294. projection.translate([globeSize / 2, globeSize / 2])
  295. .scale(globeSize / 2.3).clipAngle(90);
  296. water.attr('d', path);
  297. updateGlobe();
  298. }
  299. }
  300. function uniqueId() {
  301. function s4() {
  302. return Math.floor((1 + Math.random()) * 0x10000)
  303. .toString(16)
  304. .substring(1);
  305. }
  306. return 'point-' + s4();
  307. }
  308. }
  309. })();