ui/sliderWidget.js

  1. var svgWidget = require('./svgWidget');
  2. var inherit = require('../inherit');
  3. var registerWidget = require('../registry').registerWidget;
  4. /**
  5. * @typedef {geo.gui.widget.spec} geo.gui.sliderWidget.spec
  6. * @extends {geo.gui.widget.spec}
  7. * @property {number} [width=20] The width of the slider in pixels.
  8. * @property {number} [height=160] The height of the slider in pixels. The
  9. * actual bar is `height - 3 * width`.
  10. */
  11. /**
  12. * Create a new instance of class sliderWidget.
  13. *
  14. * @class
  15. * @alias geo.gui.sliderWidget
  16. * @extends geo.gui.svgWidget
  17. * @param {geo.gui.sliderWidget.spec} arg Options for the widget.
  18. * @returns {geo.gui.sliderWidget}
  19. */
  20. var sliderWidget = function (arg) {
  21. 'use strict';
  22. if (!(this instanceof sliderWidget)) {
  23. return new sliderWidget(arg);
  24. }
  25. svgWidget.call(this, arg);
  26. var d3 = require('../svg/svgRenderer').d3;
  27. var geo_event = require('../event');
  28. var m_this = this,
  29. s_exit = this._exit,
  30. m_xscale,
  31. m_yscale,
  32. m_plus,
  33. m_minus,
  34. m_nub,
  35. m_width = arg.width || 20, // Size of the widget in pixels
  36. m_height = arg.height || 160, // slider height + 3 * width
  37. m_nubSize = arg.width ? arg.width * 0.5 : 10,
  38. m_plusIcon,
  39. m_minusIcon,
  40. m_group,
  41. m_lowContrast,
  42. m_highlightDur = 100;
  43. /* http://icomoon.io */
  44. /* CC BY 3.0 http://creativecommons.org/licenses/by/3.0/ */
  45. m_plusIcon = 'M512 81.92c-237.568 0-430.080 192.614-430.080 430.080 0 237.568 192.563 430.080 430.080 430.080s430.080-192.563 430.080-430.080c0-237.517-192.563-430.080-430.080-430.080zM564.326 564.326v206.182h-104.653v-206.182h-206.234v-104.653h206.182v-206.234h104.704v206.182h206.182v104.704h-206.182z';
  46. m_minusIcon = 'M512 81.92c-237.568 0-430.080 192.614-430.080 430.080 0 237.568 192.563 430.080 430.080 430.080s430.080-192.563 430.080-430.080c0-237.517-192.563-430.080-430.080-430.080zM770.56 459.674v104.704h-517.12v-104.704h517.12z';
  47. // Define off-white gray colors for low contrast ui (unselected).
  48. m_lowContrast = {
  49. white: '#f4f4f4',
  50. black: '#505050'
  51. };
  52. /**
  53. * Add an icon from a path string. Returns a d3 group element.
  54. *
  55. * @param {string} icon svg path string.
  56. * @param {d3Selection} base where to append the element.
  57. * @param {number} cx Center x-coordinate.
  58. * @param {number} cy Center y-coordinate.
  59. * @param {number} size Icon size in pixels.
  60. * @returns {d3GroupElement}
  61. */
  62. function put_icon(icon, base, cx, cy, size) {
  63. var g = base.append('g');
  64. // the scale factor
  65. var s = size / 1024;
  66. g.append('g')
  67. .append('g')
  68. .attr(
  69. 'transform',
  70. 'translate(' + cx + ',' + cy + ') scale(' + s + ') translate(-512,-512)'
  71. )
  72. .append('path')
  73. .attr('d', icon)
  74. .attr('class', 'geo-glyphicon');
  75. return g;
  76. }
  77. /**
  78. * Return the size of the widget.
  79. *
  80. * @returns {geo.screenSize}
  81. */
  82. this.size = function () {
  83. return {width: m_width, height: m_height};
  84. };
  85. /**
  86. * Initialize the slider widget.
  87. *
  88. * @returns {this}
  89. */
  90. this._init = function () {
  91. m_this._createCanvas();
  92. m_this._appendCanvasToParent();
  93. m_this.reposition();
  94. var svg = d3.select(m_this.canvas()),
  95. map = m_this.layer().map();
  96. svg.attr('width', m_width).attr('height', m_height);
  97. // create d3 scales for positioning
  98. // TODO: make customizable and responsive
  99. m_xscale = d3.scaleLinear().domain([-4, 4]).range([0, m_width]);
  100. m_yscale = d3.scaleLinear().domain([0, 1]).range([m_width * 1.5, m_height - m_width * 1.5]);
  101. // Create the main group element
  102. svg = svg.append('g').classed('geo-ui-slider', true);
  103. m_group = svg;
  104. // Create + zoom button
  105. m_plus = svg.append('g');
  106. m_plus.append('circle')
  107. .datum({
  108. fill: 'white',
  109. stroke: null
  110. })
  111. .classed('geo-zoom-in', true)
  112. .attr('cx', m_xscale(0))
  113. .attr('cy', m_yscale(0.0) - m_width + 2)
  114. .attr('r', (m_width - 2) / 2)
  115. .style('cursor', 'pointer')
  116. .on('click', function () {
  117. var z = map.zoom();
  118. map.transition({
  119. zoom: z + 1,
  120. ease: d3.easeCubicInOut,
  121. duration: 500
  122. });
  123. })
  124. .on('mousedown', function (evt) {
  125. evt.stopPropagation();
  126. });
  127. put_icon(
  128. m_plusIcon,
  129. m_plus,
  130. m_xscale(0),
  131. m_yscale(0) - m_width + 2,
  132. m_width + 4
  133. ).style('cursor', 'pointer')
  134. .style('pointer-events', 'none')
  135. .select('path')
  136. .datum({
  137. fill: 'black',
  138. stroke: null
  139. });
  140. // Create the - zoom button
  141. m_minus = svg.append('g');
  142. m_minus.append('circle')
  143. .datum({
  144. fill: 'white',
  145. stroke: null
  146. })
  147. .classed('geo-zoom-out', true)
  148. .attr('cx', m_xscale(0))
  149. .attr('cy', m_yscale(1.0) + m_width - 2)
  150. .attr('r', (m_width - 2) / 2)
  151. .style('cursor', 'pointer')
  152. .on('click', function () {
  153. var z = map.zoom();
  154. map.transition({
  155. zoom: z - 1,
  156. ease: d3.easeCubicInOut,
  157. duration: 500
  158. });
  159. })
  160. .on('mousedown', function (evt) {
  161. evt.stopPropagation();
  162. });
  163. put_icon(
  164. m_minusIcon,
  165. m_minus,
  166. m_xscale(0),
  167. m_yscale(1) + m_width - 2,
  168. m_width + 4
  169. ).style('cursor', 'pointer')
  170. .style('pointer-events', 'none')
  171. .select('path')
  172. .datum({
  173. fill: 'black',
  174. stroke: null
  175. });
  176. /**
  177. * Respond to a mouse event on the widget.
  178. *
  179. * @param {d3Event} evt The event on the widget.
  180. * @param {boolean} [trans] Truthy for an animated transition.
  181. */
  182. function respond(evt, trans) {
  183. var z = m_yscale.invert(d3.pointer(event, svg.node())[1]),
  184. zrange = map.zoomRange();
  185. z = (1 - z) * (zrange.max - zrange.min) + zrange.min;
  186. if (trans) {
  187. map.transition({
  188. zoom: z,
  189. ease: d3.easeCubicInOut,
  190. duration: 500,
  191. done: m_this._update()
  192. });
  193. } else {
  194. map.zoom(z);
  195. m_this._update();
  196. }
  197. evt.stopPropagation();
  198. }
  199. // Create the track
  200. svg.append('rect')
  201. .datum({
  202. fill: 'white',
  203. stroke: 'black'
  204. })
  205. .classed('geo-zoom-track', true)
  206. .attr('x', m_xscale(0) - m_width / 6)
  207. .attr('y', m_yscale(0))
  208. .attr('rx', m_width / 10)
  209. .attr('ry', m_width / 10)
  210. .attr('width', m_width / 3)
  211. .attr('height', m_height - m_width * 3)
  212. .style('cursor', 'pointer')
  213. .on('click', function (evt) {
  214. respond(evt, true);
  215. });
  216. // Create the nub
  217. m_nub = svg.append('rect')
  218. .datum({
  219. fill: 'black',
  220. stroke: null
  221. })
  222. .classed('geo-zoom-nub', true)
  223. .attr('x', m_xscale(-4))
  224. .attr('y', m_yscale(0.5) - m_nubSize / 2)
  225. .attr('rx', 3)
  226. .attr('ry', 3)
  227. .attr('width', m_width)
  228. .attr('height', m_nubSize)
  229. .style('cursor', 'pointer')
  230. .on('mousedown', function (evt) {
  231. d3.select(document).on('mousemove.geo.slider', function () {
  232. respond(evt);
  233. });
  234. d3.select(document).on('mouseup.geo.slider', function () {
  235. respond(evt);
  236. d3.select(document).on('.geo.slider', null);
  237. });
  238. evt.stopPropagation();
  239. });
  240. /**
  241. * When the mouse is over the widget, change the style.
  242. */
  243. function mouseOver() {
  244. d3.select(this).attr('filter', 'url(#geo-highlight)');
  245. m_group.selectAll('rect,path,circle').transition()
  246. .duration(m_highlightDur)
  247. .style('fill', function (d) {
  248. return d.fill || null;
  249. })
  250. .style('stroke', function (d) {
  251. return d.stroke || null;
  252. });
  253. }
  254. /**
  255. * When the mouse is no longer over the widget, change the style.
  256. */
  257. function mouseOut() {
  258. d3.select(this).attr('filter', null);
  259. m_group.selectAll('circle,rect,path').transition()
  260. .duration(m_highlightDur)
  261. .style('fill', function (d) {
  262. return m_lowContrast[d.fill] || null;
  263. })
  264. .style('stroke', function (d) {
  265. return m_lowContrast[d.stroke] || null;
  266. });
  267. }
  268. m_group.selectAll('*')
  269. .on('mouseover', mouseOver)
  270. .on('mouseout', mouseOut);
  271. // Update the nub position on zoom
  272. m_this.geoOn(geo_event.zoom, m_this._update);
  273. mouseOut();
  274. m_this._update();
  275. return m_this;
  276. };
  277. /**
  278. * Removes the slider element from the map and unbinds all handlers.
  279. */
  280. this._exit = function () {
  281. m_this.geoOff(geo_event.zoom, m_this._update);
  282. m_group.remove();
  283. s_exit();
  284. };
  285. /**
  286. * Update the slider widget state in response to map changes. I.e., zoom
  287. * range changes.
  288. *
  289. * @param {object} [obj] An object that can specify a zoom value.
  290. * @param {number} [obj.zoom] The new zoom value to show on the slider.
  291. */
  292. this._update = function (obj) {
  293. var map = m_this.layer().map(),
  294. zoomRange = map.zoomRange(),
  295. zoom = map.zoom(),
  296. zoomScale = d3.scaleLinear();
  297. obj = obj || {};
  298. zoom = obj.value || zoom;
  299. zoomScale.domain([zoomRange.min, zoomRange.max])
  300. .range([1, 0])
  301. .clamp(true);
  302. m_nub.attr('y', m_yscale(zoomScale(zoom)) - m_nubSize / 2);
  303. };
  304. };
  305. inherit(sliderWidget, svgWidget);
  306. registerWidget('dom', 'slider', sliderWidget);
  307. module.exports = sliderWidget;