diff --git a/css/ol.css b/css/ol.css index 1a2ede8923..d6a24690d6 100644 --- a/css/ol.css +++ b/css/ol.css @@ -95,3 +95,31 @@ .ol-zoom-out:before { content: "\2212"; } + +.ol-zoomslider { + position: absolute; + top: 67px; + left: 8px; + background: rgba(255, 255, 255, 0.4); + border-radius: 4px; + width: 28px; + height: 200px; + outline: none; + overflow: hidden; + padding: 0; + margin: 0; +} +.ol-zoomslider-thumb { + position: absolute; + display: block; + padding: 0; + margin: 2px; + background: #130085; /* @alternate */ + background: rgba(0,60,136,0.5); + filter: alpha(opacity=80); + border-radius: 2px; + outline: none; + overflow: hidden; + height: 20px; + width: 24px; +} diff --git a/examples/zoomslider.html b/examples/zoomslider.html new file mode 100644 index 0000000000..437c5812b3 --- /dev/null +++ b/examples/zoomslider.html @@ -0,0 +1,115 @@ + + + + + + + + + + + ol3 ZoomSlider demo + + + + + +
+ +
+
+

Default style

+
+
+
+

Placed between zoom controls

+
+
+
+

Horizontal and completely re-styled

+
+
+
+ +
+ +
+

ZoomSlider control

+

Example of various ZoomSlider controls.

+
+

+ See the zoomslider.js + source to see how this is done. +

+
+
+ zoom, zoomslider, slider, style, styling, css, control +
+
+
+
+ + + + \ No newline at end of file diff --git a/examples/zoomslider.js b/examples/zoomslider.js new file mode 100644 index 0000000000..56cd02c210 --- /dev/null +++ b/examples/zoomslider.js @@ -0,0 +1,39 @@ +goog.require('ol.Collection'); +goog.require('ol.Coordinate'); +goog.require('ol.Map'); +goog.require('ol.View2D'); +goog.require('ol.control.ZoomSlider'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.source.MapQuestOpenAerial'); + + +/** + * Helper method for map-creation. + * + * @param {string} divId The id of the div for the map. + * @return {ol.Map} The ol.Map instance. + */ +var createMap = function(divId) { + var layer, map, zoomslider; + layer = new ol.layer.TileLayer({ + source: new ol.source.MapQuestOpenAerial() + }); + map = new ol.Map({ + layers: new ol.Collection([layer]), + target: divId, + view: new ol.View2D({ + center: new ol.Coordinate(0, 0), + zoom: 2 + }) + }); + zoomslider = new ol.control.ZoomSlider({ + minResolution: 500, + maxResolution: 100000, + map: map + }); + return map; +}; + +var map1 = createMap('map1'); +var map2 = createMap('map2'); +var map3 = createMap('map3'); diff --git a/src/objectliterals.exports b/src/objectliterals.exports index 09a9e5fe10..0e5d215c37 100644 --- a/src/objectliterals.exports +++ b/src/objectliterals.exports @@ -70,6 +70,11 @@ @exportObjectLiteralProperty ol.control.ZoomOptions.map ol.Map|undefined @exportObjectLiteralProperty ol.control.ZoomOptions.target Element|undefined +@exportObjectLiteral ol.control.ZoomSliderOptions +@exportObjectLiteralProperty ol.control.ZoomSliderOptions.map ol.Map|undefined +@exportObjectLiteralProperty ol.control.ZoomSliderOptions.maxResolution number|undefined +@exportObjectLiteralProperty ol.control.ZoomSliderOptions.minResolution number|undefined + @exportObjectLiteral ol.interaction.DefaultOptions @exportObjectLiteralProperty ol.interaction.DefaultOptions.doubleClickZoom boolean|undefined @exportObjectLiteralProperty ol.interaction.DefaultOptions.dragPan boolean|undefined diff --git a/src/ol/control/zoomslider.exports b/src/ol/control/zoomslider.exports new file mode 100644 index 0000000000..cbc760930a --- /dev/null +++ b/src/ol/control/zoomslider.exports @@ -0,0 +1 @@ +@exportClass ol.control.ZoomSlider ol.control.ZoomSliderOptions \ No newline at end of file diff --git a/src/ol/control/zoomslidercontrol.js b/src/ol/control/zoomslidercontrol.js new file mode 100644 index 0000000000..2ecb78f55c --- /dev/null +++ b/src/ol/control/zoomslidercontrol.js @@ -0,0 +1,351 @@ +// FIXME works for View2D only +// FIXME should possibly show tooltip when dragging? +// FIXME should possibly be adjustable by clicking on container + +goog.provide('ol.control.ZoomSlider'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('goog.events'); +goog.require('goog.fx.Dragger'); +goog.require('goog.style'); +goog.require('ol.FrameState'); +goog.require('ol.MapEventType'); +goog.require('ol.control.Control'); + + + +/** + * @constructor + * @extends {ol.control.Control} + * @param {ol.control.ZoomSliderOptions} zoomSliderOptions Zoom options. + */ +ol.control.ZoomSlider = function(zoomSliderOptions) { + // FIXME these should be read out from a map if not given, and only then + // fallback to the constants if they weren't defined on the map. + /** + * The minimum resolution that this control that one can set with this + * control. + * + * @type {number} + * @private + */ + this.maxResolution_ = goog.isDef(zoomSliderOptions.maxResolution) ? + zoomSliderOptions.maxResolution : + ol.control.ZoomSlider.DEFAULT_MAX_RESOLUTION; + + /** + * The maximum resolution that this control that one can set with this + * control. + * + * @type {number} + * @private + */ + this.minResolution_ = goog.isDef(zoomSliderOptions.minResolution) ? + zoomSliderOptions.minResolution : + ol.control.ZoomSlider.DEFAULT_MIN_RESOLUTION; + + goog.asserts.assert( + this.minResolution_ < this.maxResolution_, + 'minResolution must be smaller than maxResolution.' + ); + + /** + * The range of resolutions we are handling in this slider. + * + * @type {number} + * @private + */ + this.range_ = this.maxResolution_ - this.minResolution_; + + /** + * Will hold the current resolution of the view. + * + * @type {number} + * @private + */ + this.currentResolution_; + + /** + * The direction of the slider. Will be determined from actual display of the + * container and defaults to ol.control.ZoomSlider.direction.VERTICAL. + * + * @type {ol.control.ZoomSlider.direction} + * @private + */ + this.direction_ = ol.control.ZoomSlider.direction.VERTICAL; + + var elem = this.createDom_(); + this.dragger_ = this.createDraggable_(elem); + + // FIXME currently only a do nothing function is bound. + goog.events.listen(elem, [ + goog.events.EventType.TOUCHEND, + goog.events.EventType.CLICK + ], this.handleContainerClick_, false, this); + + goog.base(this, { + element: elem, + map: zoomSliderOptions.map + }); +}; +goog.inherits(ol.control.ZoomSlider, ol.control.Control); + + +/** + * The enum for available directions. + * + * @enum {number} + */ +ol.control.ZoomSlider.direction = { + VERTICAL: 0, + HORIZONTAL: 1 +}; + + +/** + * The CSS class that we'll give the zoomslider container. + * + * @const {string} + */ +ol.control.ZoomSlider.CSS_CLASS_CONTAINER = 'ol-zoomslider'; + + +/** + * The CSS class that we'll give the zoomslider thumb. + * + * @const {string} + */ +ol.control.ZoomSlider.CSS_CLASS_THUMB = + ol.control.ZoomSlider.CSS_CLASS_CONTAINER + '-thumb'; + + +/** + * The default value for minResolution_ when the control isn't instanciated with + * an explicit value. + * + * @const {number} + */ +ol.control.ZoomSlider.DEFAULT_MIN_RESOLUTION = 500; + + +/** + * The default value for maxResolution_ when the control isn't instanciated with + * an explicit value. + * + * @const {number} + */ +ol.control.ZoomSlider.DEFAULT_MAX_RESOLUTION = 1000000; + + +/** + * @inheritDoc + */ +ol.control.ZoomSlider.prototype.setMap = function(map) { + goog.base(this, 'setMap', map); + this.currentResolution_ = map.getView().getResolution(); + this.initMapEventListeners_(); + this.initSlider_(); + this.positionThumbForResolution_(this.currentResolution_); +}; + + +/** + * Initializes the event listeners for map events. + * + * @private + */ +ol.control.ZoomSlider.prototype.initMapEventListeners_ = function() { + goog.events.listen(this.getMap(), ol.MapEventType.POSTRENDER, + this.handleMapPostRender_, undefined, this); +}; + + +/** + * Initializes the slider element. This will determine and set this controls + * direction_ and also constrain the dragging of the thumb to always be within + * the bounds of the container. + * + * @private + */ +ol.control.ZoomSlider.prototype.initSlider_ = function() { + var container = this.element, + thumb = goog.dom.getFirstElementChild(container), + elemBounds = goog.style.getBounds(container), + elemPaddings = goog.style.getPaddingBox(container), + elemBorderBox = goog.style.getBorderBox(container), + thumbBounds = goog.style.getBounds(thumb), + thumbMargins = goog.style.getMarginBox(thumb), + thumbBorderBox = goog.style.getBorderBox(thumb), + w = elemBounds.width - + elemPaddings.left - elemPaddings.right - + thumbMargins.left - thumbMargins.right - + thumbBorderBox.left - thumbBorderBox.right - + thumbBounds.width, + h = elemBounds.height - + elemPaddings.top - elemPaddings.bottom - + thumbMargins.top - thumbMargins.bottom - + thumbBorderBox.top - thumbBorderBox.bottom - + thumbBounds.height, + limits; + // set the direction_ of the zoomslider and the allowed bounds for dragging + if (elemBounds.width > elemBounds.height) { + this.direction_ = ol.control.ZoomSlider.direction.HORIZONTAL; + limits = new goog.math.Rect(0, 0, w, 0); + } else { + this.direction_ = ol.control.ZoomSlider.direction.VERTICAL; + limits = new goog.math.Rect(0, 0, 0, h); + } + this.dragger_.setLimits(limits); +}; + + +/** + * @param {{frameState:ol.FrameState}} evtObj The evtObj. + * @private + */ +ol.control.ZoomSlider.prototype.handleMapPostRender_ = function(evtObj) { + var res = evtObj.frameState.view2DState.resolution; + if (res !== this.currentResolution_) { + this.currentResolution_ = res; + this.positionThumbForResolution_(res); + } +}; + + +/** + * @param {goog.events.BrowserEvent} browserEvent The browser event to handle. + * @private + */ +ol.control.ZoomSlider.prototype.handleContainerClick_ = function(browserEvent) { + // TODO implement proper resolution calculation according to browserEvent +}; + + +/** + * Positions the thumb inside its container according to the given resolution. + * + * @param {number} res The res. + * @private + */ +ol.control.ZoomSlider.prototype.positionThumbForResolution_ = function(res) { + var amount = this.amountForResolution_(res), + dragger = this.dragger_, + thumb = goog.dom.getFirstElementChild(this.element); + + if (this.direction_ == ol.control.ZoomSlider.direction.HORIZONTAL) { + var left = dragger.limits.left + dragger.limits.width * amount; + goog.style.setPosition(thumb, left); + } else { + var top = dragger.limits.top + dragger.limits.height * amount; + goog.style.setPosition(thumb, dragger.limits.left, top); + } +}; + + +/** + * Calculates the amount the thumb has been dragged to allow for calculation + * of the corresponding resolution. + * + * @param {goog.fx.DragDropEvent} e The dragdropevent. + * @return {number} The amount the thumb has been dragged. + * @private + */ +ol.control.ZoomSlider.prototype.amountDragged_ = function(e) { + var draggerLimits = this.dragger_.limits, + amount = 0; + if (this.direction_ === ol.control.ZoomSlider.direction.HORIZONTAL) { + amount = (e.left - draggerLimits.left) / draggerLimits.width; + } else { + amount = (e.top - draggerLimits.top) / draggerLimits.height; + } + return amount; +}; + + +/** + * Calculates the corresponding resolution of the thumb by the amount it has + * been dragged from its minimum. + * + * @param {number} amount The amount the thumb has been dragged. + * @return {number} a resolution between this.minResolution_ and + * this.maxResolution_. + * @private + */ +ol.control.ZoomSlider.prototype.resolutionForAmount_ = function(amount) { + var saneAmount = goog.math.clamp(amount, 0, 1); + return this.minResolution_ + this.range_ * saneAmount; +}; + + +/** + * Determines an amount of dragging relative to this minimum position by the + * given resolution. + * + * @param {number} res The resolution to get the amount for. + * @return {number} an amount between 0 and 1. + * @private + */ +ol.control.ZoomSlider.prototype.amountForResolution_ = function(res) { + var saneRes = goog.math.clamp(res, this.minResolution_, this.maxResolution_); + return (saneRes - this.minResolution_) / this.range_; +}; + + +/** + * Handles the user caused changes of the slider thumb and adjusts the + * resolution of our map accordingly. Will be called both while dragging and + * when dragging ends. + * + * @param {goog.fx.DragDropEvent} e The dragdropevent. + * @private + */ +ol.control.ZoomSlider.prototype.handleSliderChange_ = function(e) { + var map = this.getMap(), + amountDragged = this.amountDragged_(e), + res = this.resolutionForAmount_(amountDragged); + if (res !== this.currentResolution_) { + this.currentResolution_ = res; + map.getView().setResolution(res); + } +}; + + +/** + * Actually enable draggable behaviour for the thumb of the zoomslider and bind + * relvant event listeners. + * + * @param {Element} elem The element for the slider. + * @return {goog.fx.Dragger} The actual goog.fx.Dragger instance. + * @private + */ +ol.control.ZoomSlider.prototype.createDraggable_ = function(elem) { + var dragger = new goog.fx.Dragger(elem.childNodes[0]); + dragger.addEventListener(goog.fx.Dragger.EventType.DRAG, + this.handleSliderChange_, undefined, this); + dragger.addEventListener(goog.fx.Dragger.EventType.END, + this.handleSliderChange_, undefined, this); + return dragger; +}; + + +/** + * Setup the DOM-structure we need for the zoomslider. + * + * @param {Element=} opt_elem The element for the slider. + * @return {Element} The correctly set up DOMElement. + * @private + */ +ol.control.ZoomSlider.prototype.createDom_ = function(opt_elem) { + var elem, + sliderCssCls = ol.control.ZoomSlider.CSS_CLASS_CONTAINER + + ' ol-unselectable', + thumbCssCls = ol.control.ZoomSlider.CSS_CLASS_THUMB + + ' ol-unselectable'; + + elem = goog.dom.createDom(goog.dom.TagName.DIV, sliderCssCls, + goog.dom.createDom(goog.dom.TagName.DIV, thumbCssCls)); + + return elem; +}; diff --git a/test/spec/ol/control/zoomslider.test.js b/test/spec/ol/control/zoomslider.test.js new file mode 100644 index 0000000000..da5010da60 --- /dev/null +++ b/test/spec/ol/control/zoomslider.test.js @@ -0,0 +1,107 @@ +goog.provide('ol.test.control.ZoomSlider'); + +describe('ol.control.ZoomSlider', function() { + var map, zoomslider; + + beforeEach(function() { + map = new ol.Map({ + target: document.getElementById('map') + }); + zoomslider = new ol.control.ZoomSlider({ + minResolution: 5000, + maxResolution: 100000, + map: map + }); + }); + + afterEach(function() { + zoomslider.dispose(); + map.dispose(); + }); + + describe('configuration & defaults', function() { + + it('has valid defaults for min and maxresolution', function() { + expect(function() { + zoomslider = new ol.control.ZoomSlider({}); + }).not.toThrow(); + expect(zoomslider.minResolution_).toBe(500); + expect(zoomslider.maxResolution_).toBe(1000000); + expect(zoomslider.range_).toBe(999500); + }); + + it('throws exception when configured with wrong resolutions', function() { + expect(function() { + zoomslider = new ol.control.ZoomSlider({ + minResolution: 50, + maxResolution: 0 + }); + }).toThrow(); + }); + + it('can be configured with valid resolutions', function() { + expect(function() { + zoomslider = new ol.control.ZoomSlider({ + minResolution: 790, + maxResolution: 91000 + }); + }).not.toThrow(); + expect(zoomslider.minResolution_).toBe(790); + expect(zoomslider.maxResolution_).toBe(91000); + expect(zoomslider.range_).toBe(90210); + }); + }); + + describe('DOM creation', function() { + it('creates the expected DOM elements', function() { + var zoomSliderContainers = goog.dom.getElementsByClass('ol-zoomslider'), + zoomSliderContainer, + zoomSliderThumbs, + zoomSliderThumb, + hasUnselectableCls; + + expect(zoomSliderContainers.length).toBe(1); + + zoomSliderContainer = zoomSliderContainers[0]; + expect(zoomSliderContainer instanceof HTMLDivElement).toBe(true); + + hasUnselectableCls = goog.dom.classes.has(zoomSliderContainer, + 'ol-unselectable'); + expect(hasUnselectableCls).toBe(true); + + zoomSliderThumbs = goog.dom.getElementsByClass('ol-zoomslider-thumb', + zoomSliderContainer); + expect(zoomSliderThumbs.length).toBe(1); + + zoomSliderThumb = zoomSliderThumbs[0]; + expect(zoomSliderThumb instanceof HTMLDivElement).toBe(true); + + hasUnselectableCls = goog.dom.classes.has(zoomSliderThumb, + 'ol-unselectable'); + expect(hasUnselectableCls).toBe(true); + }); + + }); + + describe('dragger setup', function() { + it('creates a goog.fx.Dragger', function() { + expect(zoomslider.dragger_).toBeDefined(); + expect(zoomslider.dragger_).toBeA(goog.fx.Dragger); + + expect(zoomslider.dragger_.limits).toBeDefined(); + expect(zoomslider.dragger_.limits).toBeA(goog.math.Rect); + + expect(zoomslider.direction_).toBeDefined(); + expect(zoomslider.direction_).toBe(1); // vertical + }); + }); + +}); + +goog.require('goog.dom'); +goog.require('goog.dom.classes'); +goog.require('goog.fx.Dragger'); +goog.require('goog.math.Rect'); +goog.require('goog.style'); +goog.require('ol.Map'); +goog.require('ol.control.ZoomSlider');