From b812c070f127e8aacf6dd5dc2f5c0cae696ad3ac Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 7 Aug 2012 16:50:22 -0400 Subject: [PATCH] Replace standard PanZoomBar control with new SimplePanZoom control --- app/assets/javascripts/map.js.erb | 2 +- app/assets/javascripts/openlayers.js.erb | 1 + app/assets/openlayers/SimplePanZoom.js | 355 ++++++++++++++++++ .../openstreetmap/SimplePanZoom.css.scss | 76 ++++ .../theme/openstreetmap/img/map_sprite.png | Bin 0 -> 8352 bytes .../theme/openstreetmap/style.css.scss | 1 + app/assets/stylesheets/small.css.scss | 2 +- 7 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 app/assets/openlayers/SimplePanZoom.js create mode 100644 app/assets/openlayers/theme/openstreetmap/SimplePanZoom.css.scss create mode 100644 app/assets/openlayers/theme/openstreetmap/img/map_sprite.png diff --git a/app/assets/javascripts/map.js.erb b/app/assets/javascripts/map.js.erb index 82826ebfb..50b0fe441 100644 --- a/app/assets/javascripts/map.js.erb +++ b/app/assets/javascripts/map.js.erb @@ -14,7 +14,7 @@ function createMap(divName, options) { new SimpleLayerSwitcher(), new OpenLayers.Control.Navigation(), new OpenLayers.Control.Zoom(), - new OpenLayers.Control.PanZoomBar(), + new OpenLayers.Control.SimplePanZoom(), new OpenLayers.Control.ScaleLine({geodesic: true}) ], numZoomLevels: 20, diff --git a/app/assets/javascripts/openlayers.js.erb b/app/assets/javascripts/openlayers.js.erb index b493a0517..ebb9578a3 100644 --- a/app/assets/javascripts/openlayers.js.erb +++ b/app/assets/javascripts/openlayers.js.erb @@ -1,6 +1,7 @@ //= require OpenLayers //= require OpenStreetMap //= require SimpleLayerSwitcher +//= require SimplePanZoom OpenLayers.Util.imageURLs = { "404.png": "<%= asset_path 'img/404.png' %>", diff --git a/app/assets/openlayers/SimplePanZoom.js b/app/assets/openlayers/SimplePanZoom.js new file mode 100644 index 000000000..a1a389d90 --- /dev/null +++ b/app/assets/openlayers/SimplePanZoom.js @@ -0,0 +1,355 @@ +/* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the 2-clause BSD license. + * See license.txt in the OpenLayers distribution or repository for the + * full text of the license. */ + + +/** + * @requires OpenLayers/Control/PanZoom.js + */ + +/** + * Class: OpenLayers.Control.PanZoomBar + * The PanZoomBar is a visible control composed of a + * and a . + * By default it is displayed in the upper left corner of the map as 4 + * directional arrows above a vertical slider. + * + * Inherits from: + * - + */ +OpenLayers.Control.SimplePanZoom = OpenLayers.Class(OpenLayers.Control.PanZoom, { + + /** + * APIProperty: zoomStopWidth + */ + zoomStopWidth: 18, + + /** + * APIProperty: zoomStopHeight + */ + zoomStopHeight: 7, + + /** + * Property: slider + */ + slider: null, + + /** + * Property: sliderEvents + * {} + */ + sliderEvents: null, + + /** + * Property: zoombarDiv + * {DOMElement} + */ + zoombarDiv: null, + + /** + * APIProperty: zoomWorldIcon + * {Boolean} + */ + zoomWorldIcon: false, + + /** + * APIProperty: panIcons + * {Boolean} Set this property to false not to display the pan icons. If + * false the zoom world icon is placed under the zoom bar. Defaults to + * true. + */ + panIcons: true, + + /** + * APIProperty: forceFixedZoomLevel + * {Boolean} Force a fixed zoom level even though the map has + * fractionalZoom + */ + forceFixedZoomLevel: false, + + /** + * Property: mouseDragStart + * {} + */ + mouseDragStart: null, + + /** + * Property: deltaY + * {Number} The cumulative vertical pixel offset during a zoom bar drag. + */ + deltaY: null, + + /** + * Property: zoomStart + * {} + */ + zoomStart: null, + + /** + * Constructor: OpenLayers.Control.PanZoomBar + */ + buttons: null, + + /** + * APIMethod: destroy + */ + destroy: function() { + + this._removeZoomBar(); + + this.map.events.un({ + "changebaselayer": this.redraw, + "updatesize": this.redraw, + scope: this + }); + + OpenLayers.Control.PanZoom.prototype.destroy.apply(this, arguments); + + delete this.mouseDragStart; + delete this.zoomStart; + }, + + /** + * Method: setMap + * + * Parameters: + * map - {} + */ + setMap: function(map) { + OpenLayers.Control.PanZoom.prototype.setMap.apply(this, arguments); + this.map.events.on({ + "changebaselayer": this.redraw, + "updatesize": this.redraw, + scope: this + }); + }, + + /** + * Method: redraw + * clear the div and start over. + */ + redraw: function() { + if (this.div !== null) { + this.removeButtons(); + this._removeZoomBar(); + } + this.draw(); + }, + + /** + * Method: draw + * + * Parameters: + * px - {} + */ + draw: function(px) { + // initialize our internal div + OpenLayers.Control.prototype.draw.apply(this, arguments); + px = this.position.clone(); + + // place the controls + this.buttons = []; + var ids = ['panup', 'panleft', 'panright', 'pandown', 'zoomout', 'zoomin']; + + for (var i = 0; i < ids.length; i++) { + var b = document.createElement('div'); + b.id = ids[i]; + b.action = ids[i]; + b.className = 'button olButton'; + this.div.appendChild(b); + this.buttons.push(b); + } + + this._addZoomBar(); + return this.div; + }, + + /** + * Method: _addZoomBar + * + * Parameters: + * centered - {} where zoombar drawing is to start. + */ + _addZoomBar:function() { + var id = this.id + "_" + this.map.id; + var zoomsToEnd = this.map.getNumZoomLevels() - 1 - this.map.getZoom(); + var slider = document.createElement('div'); + slider.id = 'slider'; + slider.className = 'button'; + slider.style.cursor = 'move'; + this.slider = slider; + + this.sliderEvents = new OpenLayers.Events(this, slider, null, true, + { includeXY: true }); + this.sliderEvents.on({ + "touchstart": this.zoomBarDown, + "touchmove": this.zoomBarDrag, + "touchend": this.zoomBarUp, + "mousedown": this.zoomBarDown, + "mousemove": this.zoomBarDrag, + "mouseup": this.zoomBarUp + }); + + var height = this.zoomStopHeight * (this.map.getNumZoomLevels()); + + // this is the background image + var div = document.createElement('div'); + div.className = 'button olButton'; + div.id = 'zoombar'; + this.zoombarDiv = div; + + this.div.appendChild(div); + this.startTop = 75; + this.div.appendChild(slider); + + this.map.events.register("zoomend", this, this.moveZoomBar); + }, + + /** + * Method: _removeZoomBar + */ + _removeZoomBar: function() { + this.sliderEvents.un({ + "touchstart": this.zoomBarDown, + "touchmove": this.zoomBarDrag, + "touchend": this.zoomBarUp, + "mousedown": this.zoomBarDown, + "mousemove": this.zoomBarDrag, + "mouseup": this.zoomBarUp + }); + this.sliderEvents.destroy(); + + this.div.removeChild(this.zoombarDiv); + this.zoombarDiv = null; + this.div.removeChild(this.slider); + this.slider = null; + + this.map.events.unregister("zoomend", this, this.moveZoomBar); + }, + + /** + * Method: onButtonClick + * + * Parameters: + * evt - {Event} + */ + onButtonClick: function(evt) { + OpenLayers.Control.PanZoom.prototype.onButtonClick.apply(this, arguments); + if (evt.buttonElement === this.zoombarDiv) { + var levels = evt.buttonXY.y / this.zoomStopHeight; + if (this.forceFixedZoomLevel || !this.map.fractionalZoom) { + levels = Math.floor(levels); + } + var zoom = (this.map.getNumZoomLevels() - 1) - levels; + zoom = Math.min(Math.max(zoom, 0), this.map.getNumZoomLevels() - 1); + this.map.zoomTo(zoom); + } + }, + + /** + * Method: passEventToSlider + * This function is used to pass events that happen on the div, or the map, + * through to the slider, which then does its moving thing. + * + * Parameters: + * evt - {} + */ + passEventToSlider:function(evt) { + this.sliderEvents.handleBrowserEvent(evt); + }, + + /* + * Method: zoomBarDown + * event listener for clicks on the slider + * + * Parameters: + * evt - {} + */ + zoomBarDown:function(evt) { + if (!OpenLayers.Event.isLeftClick(evt) && !OpenLayers.Event.isSingleTouch(evt)) { + return; + } + this.map.events.on({ + "touchmove": this.passEventToSlider, + "mousemove": this.passEventToSlider, + "mouseup": this.passEventToSlider, + scope: this + }); + this.mouseDragStart = evt.xy.clone(); + this.zoomStart = evt.xy.clone(); + this.div.style.cursor = "move"; + // reset the div offsets just in case the div moved + this.zoombarDiv.offsets = null; + OpenLayers.Event.stop(evt); + }, + + /* + * Method: zoomBarDrag + * This is what happens when a click has occurred, and the client is + * dragging. Here we must ensure that the slider doesn't go beyond the + * bottom/top of the zoombar div, as well as moving the slider to its new + * visual location + * + * Parameters: + * evt - {} + */ + zoomBarDrag: function(evt) { + if (this.mouseDragStart !== null) { + var deltaY = this.mouseDragStart.y - evt.xy.y; + var offsets = OpenLayers.Util.pagePosition(this.zoombarDiv); + if ((evt.clientY - offsets[1]) > 0 && + (evt.clientY - offsets[1]) < 140) { + var newTop = parseInt(this.slider.style.top, 10) - deltaY; + this.slider.style.top = newTop + "px"; + this.mouseDragStart = evt.xy.clone(); + } + // set cumulative displacement + this.deltaY = this.zoomStart.y - evt.xy.y; + OpenLayers.Event.stop(evt); + } + }, + + /* + * Method: zoomBarUp + * Perform cleanup when a mouseup event is received -- discover new zoom + * level and switch to it. + * + * Parameters: + * evt - {} + */ + zoomBarUp: function(evt) { + if (!OpenLayers.Event.isLeftClick(evt) && evt.type !== "touchend") { + return; + } + if (this.mouseDragStart) { + this.div.style.cursor = ""; + this.map.events.un({ + "touchmove": this.passEventToSlider, + "mouseup": this.passEventToSlider, + "mousemove": this.passEventToSlider, + scope: this + }); + var zoomLevel = this.map.zoom; + zoomLevel += this.deltaY/this.zoomStopHeight; + zoomLevel = Math.max(Math.round(zoomLevel), 0); + this.map.zoomTo(zoomLevel); + this.mouseDragStart = null; + this.zoomStart = null; + this.deltaY = 0; + OpenLayers.Event.stop(evt); + } + }, + + /* + * Method: moveZoomBar + * Change the location of the slider to match the current zoom level. + */ + moveZoomBar:function() { + var newTop = + ((this.map.getNumZoomLevels()-1) - this.map.getZoom()) * + this.zoomStopHeight + this.startTop; + this.slider.style.top = newTop + "px"; + }, + CLASS_NAME: "OpenLayers.Control.SimplePanZoom" +}); diff --git a/app/assets/openlayers/theme/openstreetmap/SimplePanZoom.css.scss b/app/assets/openlayers/theme/openstreetmap/SimplePanZoom.css.scss new file mode 100644 index 000000000..898640ccb --- /dev/null +++ b/app/assets/openlayers/theme/openstreetmap/SimplePanZoom.css.scss @@ -0,0 +1,76 @@ +.olControlSimplePanZoom { + top: 10px; + right: 10px; +} + +.olControlSimplePanZoom .button { + background-image: image-url("theme/openstreetmap/img/map_sprite.png"); + position: absolute; + background-repeat: no-repeat; + cursor: hand; + cursor: pointer; +} + +.olControlSimplePanZoom #panup { + left: 10px; + width: 25px; + height: 13px; + background-position: -15px -5px; +} + +.olControlSimplePanZoom #pandown { + left: 10px; + top: 36px; + width: 25px; + height: 15px; + background-position: -15px -40px; +} + +.olControlSimplePanZoom #panleft { + top: 13px; + width: 25px; + height: 24px; + background-position: -5px -17px; +} + +.olControlSimplePanZoom #panright { + top: 13px; + width: 25px; + height: 24px; + left: 25px; + background-position: -30px -17px; +} + +.olControlSimplePanZoom #zoomin { + top: 50px; + width: 26px; + height: 20px; + left: 10px; + background-position: -15px -61px; +} + +.olControlSimplePanZoom #zoomout { + top: 210px; + width: 26px; + height: 20px; + left: 10px; + background-position: -15px -220px; +} + +.olControlSimplePanZoom #slider { + top: 75px; + width: 25px; + height: 10px; + left: 10px; + -webkit-transition: top 100ms linear; + background-position: -77px -58px; + pointer: move; + cursor: move; +} +.olControlSimplePanZoom #zoombar { + top: 70px; + width: 26px; + height: 140px; + left: 10px; + background-position: -15px -80px; +} diff --git a/app/assets/openlayers/theme/openstreetmap/img/map_sprite.png b/app/assets/openlayers/theme/openstreetmap/img/map_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..99734b12f4f5bc39a893851ac5493906cbb6655f GIT binary patch literal 8352 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 VuV1`OSp