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
+
+
+
+
+
+
+
+
+
+
+
Placed between zoom controls
+
+
+
+
Horizontal and completely re-styled
+
+
+
+
+
+
+
+
ZoomSlider control
+
Example of various ZoomSlider controls.
+
+
+ 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');