diff --git a/css/img/close.gif b/css/img/close.gif new file mode 100644 index 0000000000..a8958de9b4 Binary files /dev/null and b/css/img/close.gif differ diff --git a/css/ol.css b/css/ol.css index cf034845b8..cf46a42de2 100644 --- a/css/ol.css +++ b/css/ol.css @@ -1,2 +1,120 @@ .ol-viewport { width:100%; height:100%; position:relative; left:0; top:0; } -.ol-renderer-webgl-canvas { width:100%; height:100%; } \ No newline at end of file +.ol-renderer-webgl-canvas { width:100%;height:100%; } + +/** + * arrow implementation from http://cssarrowplease.com/ for ol-popup + */ + +.ol-popup { + position: absolute; + background: #88b7d5; + border: 4px solid #c2e1f5; +} + +/** + * FIXME + */ +.ol-popup-close { + background: url("img/close.gif") no-repeat; + cursor: pointer; + position: absolute; + width: 17px; + height: 17px; + right: 0; +} + +.ol-popup-top {} +.ol-popup-top:after, .ol-popup-top:before { + bottom: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +.ol-popup-top:after { + border-bottom-color: #88b7d5; + border-width: 30px; + left: 50%; + margin-left: -30px; +} + +.ol-popup-top:before { + border-bottom-color: #c2e1f5; + border-width: 36px; + left: 50%; + margin-left: -36px; +} + +.ol-popup-bottom {} +.ol-popup-bottom:after, .ol-popup-bottom:before { + top: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +.ol-popup-bottom:after { + border-top-color: #88b7d5; + border-width: 30px; + left: 50%; + margin-left: -30px; +} +.ol-popup-bottom:before { + border-top-color: #c2e1f5; + border-width: 36px; + left: 50%; + margin-left: -36px; +} +.ol-popup-right {} +.ol-popup-right:after, .ol-popup-right:before { + left: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +.ol-popup-right:after { + border-left-color: #88b7d5; + border-width: 30px; + top: 50%; + margin-top: -30px; +} +.ol-popup-right:before { + border-left-color: #c2e1f5; + border-width: 36px; + top: 50%; + margin-top: -36px; +} +.ol-popup-left {} +.ol-popup-left:after, .ol-popup-left:before { + right: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +.ol-popup-left:after { + border-right-color: #88b7d5; + border-width: 30px; + top: 50%; + margin-top: -30px; +} +.ol-popup-left:before { + border-right-color: #c2e1f5; + border-width: 36px; + top: 50%; + margin-top: -36px; +} + diff --git a/src/api/popup.js b/src/api/popup.js new file mode 100644 index 0000000000..703f48a07e --- /dev/null +++ b/src/api/popup.js @@ -0,0 +1,130 @@ +goog.provide('ol.popup'); + +goog.require('ol.Popup'); +goog.require('ol.map'); + + +/** + * @typedef {ol.Popup|Object} popup + */ +ol.PopupLike; + + + +/** + * @export + * @param {ol.PopupLike} opt_arg popup object literal. + * @return {ol.Popup} the popup. + */ +ol.popup = function(opt_arg){ + + if (opt_arg instanceof ol.Popup) { + return opt_arg; + } + + /** @type {ol.Map} */ + var map; + + /** @type {ol.Loc|ol.Feature|undefined} */ + var anchor; + + /** @type {string|undefined} */ + var placement; + + /** @type {string|undefined} */ + var content; + + /** @type {string|undefined} */ + var template; + + if (arguments.length == 1 && goog.isDef(opt_arg)) { + if (goog.isObject(opt_arg)) { + map = opt_arg['map']; + anchor = opt_arg['anchor']; + placement = opt_arg['placement']; + content = opt_arg['content']; + template = opt_arg['template']; + } + } + + var popup = new ol.Popup(map, anchor); + + if (goog.isDef(anchor)) { + popup.setAnchor(anchor); + } + if (goog.isDef(placement)) { + popup.setPlacement(placement); + } + if (goog.isDef(content)) { + popup.setContent(content); + } + if (goog.isDef(template)) { + popup.setTemplate(template); + } + + return popup; + +}; + + +/** + * @export + * @param {ol.Loc|ol.Feature=} opt_arg a feature or a location. + * @return {ol.Popup|ol.Feature|ol.Loc|undefined} Result. + */ +ol.Popup.prototype.anchor = function(opt_arg){ + if (arguments.length == 1 && goog.isDef(opt_arg)) { + this.setAnchor(opt_arg); + return this; + } + else { + return this.getAnchor(); + } +}; + + +/** + * @export + * @param {ol.Map=} opt_arg the map . + * @return {ol.Popup|ol.Map|undefined} the map or the popup. + */ +ol.Popup.prototype.map = function(opt_arg){ + if (arguments.length == 1 && goog.isDef(opt_arg)) { + this.setMap(opt_arg); + return this; + } + else { + return this.getMap(); + } +}; + +/** + * @export + * @param {string=} opt_arg the content for the map (HTML makrkup) + * @return {ol.Popup|string|undefined} the content or the popup. + */ +ol.Popup.prototype.content = function(opt_arg){ + if (arguments.length == 1 && goog.isDef(opt_arg)) { + this.setContent(opt_arg); + return this; + } + else { + return this.getContent(); + } +}; + +/** + * @export + * @param {string=} opt_arg the template to be used to generate the content + * @return {ol.Popup|string|undefined} the template or the popup. + */ +ol.Popup.prototype.template = function(opt_arg){ + if (arguments.length == 1 && goog.isDef(opt_arg)) { + this.setTemplate(opt_arg); + return this; + } + else { + return this.getTemplate(); + } +}; + diff --git a/src/ol.js b/src/ol.js index b8f91ab859..163edd4dcb 100644 --- a/src/ol.js +++ b/src/ol.js @@ -12,6 +12,7 @@ goog.require("ol.feature"); goog.require("ol.projection"); goog.require("ol.layer.xyz"); goog.require("ol.layer.osm"); +goog.require("ol.popup"); goog.require("ol.Tile"); goog.require("ol.TileSet"); goog.require("ol.TileCache"); diff --git a/src/ol/Map.js b/src/ol/Map.js index 5e9f3bcf7e..1352c90c88 100644 --- a/src/ol/Map.js +++ b/src/ol/Map.js @@ -385,6 +385,25 @@ ol.Map.prototype.moveByPx = function(dx, dy) { // call moveByPx on renderers }; +/** + * @param {ol.Loc} loc the location being requested + * @returns {Object} the + */ +ol.Map.prototype.getViewportPosition = function(loc) { + //TODO: delegate this to the renderers + //stub for now to get popups working + return {x: 200, y: 300}; +}; + +/** + * @returns {Element} the map overlay element + */ +ol.Map.prototype.getMapOverlay = function() { + //TODO: delegate this to the renderers + //stub for now to get popups working + return this.mapOverlay_ +}; + /** * @export */ diff --git a/src/ol/Popup.js b/src/ol/Popup.js new file mode 100644 index 0000000000..4aa843e8c7 --- /dev/null +++ b/src/ol/Popup.js @@ -0,0 +1,281 @@ +goog.provide('ol.Popup'); + +goog.require('ol.Map'); +goog.require('ol.Loc'); +goog.require('ol.Feature'); +//goog.require('goog.dom'); +//goog.require('goog.style'); + + +/** + * @export + * @constructor + * @param {ol.Map} map the map on which the popup is placed. + * @param {ol.Loc|ol.Feature=} opt_anchor the anchor object for the popup. + * @param {string=} opt_placement the placement of the arrow on the popup. + * @param {boolean=} opt_close include a close button on the popup + */ +ol.Popup = function(map, opt_anchor, opt_placement, opt_close) { + + /** + * @private + * @type {ol.Map} + */ + this.map_ = map; + + /** + * @private + * @type {ol.Loc|ol.Feature|undefined} + */ + this.anchor_ = opt_anchor; + + /** + * can be 'top','bottom','right','left','auto' + * TODO: 'auto' not yet implemented + * @private + * @type {!string} + */ + this.placement_ = goog.isDefAndNotNull(opt_placement)?opt_placement:'top'; + + /** + * include a close button on the popup - defaults to true. + * @private + * @type {boolean|undefined} + */ + this.closeButton_ = goog.isDefAndNotNull(opt_close) ? opt_close : true; + + /** + * @private + * @type {string|undefined} + */ + this.content_ = undefined; + + /** + * @private + * @type {string|undefined} + */ + this.template_ = undefined; + + /** + * @private + * @type {Element} + */ + this.container_ = null; + + /** + * @private + * @type {number} + */ + this.arrowOffset_ = 32; //FIXME: set this from CSS dynamically somehow? + +}; + +/** + * @const + */ +ol.Popup.CLASS_NAME = 'ol-popup'; + +/** + * @return {ol.Map} Projection. + */ +ol.Popup.prototype.getMap = function() { + return this.map_; +}; + +/** + * @param {ol.Map} map the map object to hold this popup. + */ +ol.Popup.prototype.setMap = function(map) { + this.map_ = map; +}; + +/** + * @return {ol.Feature|ol.Loc|undefined} the anchor . + */ +ol.Popup.prototype.getAnchor = function() { + return this.anchor_; +}; + +/** + * @param {ol.Feature|ol.Loc} anchor the anchor location to place this popup. + */ +ol.Popup.prototype.setAnchor = function(anchor) { + this.anchor_ = anchor; +}; + + +/** + * @return {string|undefined} the placement value relative to the anchor. + */ +ol.Popup.prototype.getPlacement = function() { + return this.placement_; +}; + +/** + * @param {string} placement where to place this popup relative to the anchor. + */ +ol.Popup.prototype.setPlacement = function(placement) { + if (!goog.isNull(this.container_)) { + goog.dom.classes.remove(this.container_, + ol.Popup.CLASS_NAME+'-'+this.placement_); + goog.dom.classes.add(this.container_,ol.Popup.CLASS_NAME+'-'+placement); + } + this.placement_ = placement; +}; + + +/** + * @return {string|undefined} static content to be displayed in the popup (HTML) + */ +ol.Popup.prototype.getContent = function() { + return this.content_; +}; + +/** + * @param {string} content the content to be displayed this popup. + */ +ol.Popup.prototype.setContent = function(content) { + this.content_ = content; +}; + + +/** + * @private + * @returns {string} generates the content + */ +ol.Popup.prototype.generateContent_ = function() { + //set the content + if ( goog.isDefAndNotNull(this.content_) ) { + return this.content_; + } else { + if ( goog.isDefAndNotNull(this.template_) && + (this.anchor_ instanceof ol.Feature)) { + //set content from feature attributes on the template + //TODO: this.setContent(template.apply(this.anchor.getAttributes())); + return this.template_; //stub to return something + } else { + return '
'; + } + } +}; + + +/** + * @return {string|undefined} the anchor . + */ +ol.Popup.prototype.getTemplate = function() { + return this.template_; +}; + +/** + * @param {string} template the map object to hold this popup. + */ +ol.Popup.prototype.setTemplate = function(template) { + this.template_ = template; +}; + +/** + * Open the popup. + * @export + * @param {ol.Feature|ol.Loc} opt_arg feature or location for the anchor + */ +ol.Popup.prototype.open = function(opt_arg) { + if (goog.isDef(opt_arg)) { + this.setAnchor(opt_arg); + } + + //create popup container if it's not created already + if (goog.isNull(this.container_)) { + this.container_ = goog.dom.createElement('div'); + goog.dom.classes.add(this.container_, + ol.Popup.CLASS_NAME, ol.Popup.CLASS_NAME+'-'+this.placement_); + + if (this.closeButton_) { + var closeButton = goog.dom.createElement('div'); + goog.dom.appendChild(this.container_, closeButton); + goog.dom.classes.add(closeButton, ol.Popup.CLASS_NAME+'-close'); + } + this.map_.getEvents().register('click', this.clickHandler, this); + goog.dom.appendChild(this.map_.getMapOverlay(), this.container_); + } + + this.childContent_=goog.dom.htmlToDocumentFragment(this.generateContent_()); + goog.dom.appendChild(this.container_, this.childContent_); + + //position the element + if (this.anchor_ instanceof ol.Feature) { + this.pos_ = this.anchor_.getGeometry().getCentroid(); + } else { + this.pos_ = this.anchor_; + } + var popupPosPx = this.map_.getViewportPosition(this.pos_); + var popupSize = goog.style.getSize(this.container_); + + switch(this.placement_) { + default: + case 'auto': + //TODO: switch based on map quadrant + break; + case 'top': + case 'bottom': + popupPosPx.x -= popupSize.width / 2.0; + + if (this.placement_ == "bottom") { + popupPosPx.y -= popupSize.height + this.arrowOffset_; + } else { + popupPosPx.y += this.arrowOffset_; + } + break; + case 'left': + case 'right': + popupPosPx.y -= popupSize.height / 2.0; + + if (this.placement_ == "right") { + popupPosPx.x -= popupSize.width + this.arrowOffset_; + } else { + popupPosPx.x += this.arrowOffset_; + } + break; + }; + this.moveTo_(popupPosPx); + +}; + +/** + * @param px - {goog.} the top and left position of the popup div. + */ +ol.Popup.prototype.moveTo_ = function(px) { + if (goog.isDefAndNotNull(px)) { + goog.style.setPosition(this.container_, px.x, px.y); + } +}; + +/** + * Click handler + * @param {Event} evt the event generated by a click + */ +ol.Popup.prototype.clickHandler = function(evt) { + var target = /** @type {Node} */ evt.target; + if (goog.dom.classes.has(target,ol.Popup.CLASS_NAME+'-close')) { + this.close(); + } +}; + +/** + * Clean up. + * @export + */ +ol.Popup.prototype.close = function() { + goog.dom.removeChildren(this.container_); + goog.dom.removeNode(this.container_); +}; + +/** + * Clean up. + * @export + */ +ol.Popup.prototype.destroy = function() { + for (var key in this) { + delete this[key]; + } +}; diff --git a/src/ol/event/Drag.js b/src/ol/event/Drag.js index 869fc2eb83..82fbab5827 100644 --- a/src/ol/event/Drag.js +++ b/src/ol/event/Drag.js @@ -12,13 +12,19 @@ goog.require('goog.functions'); /** * @constructor - * @param {Element} target The element that will be dragged. + * @param {ol.event.Events} target The Events instance that handles events. * @extends {goog.fx.Dragger} * @implements {ol.event.ISequence} * @export */ ol.event.Drag = function(target) { - goog.base(this, target); + goog.base(this, target.getElement()); + + /** + * @private + * @type {ol.event.Events} + */ + this.target_ = target; /** * @private @@ -47,6 +53,7 @@ ol.event.Drag.prototype.dispatchEvent = function(e) { e.type = ol.event.Drag.EventType.DRAGEND; } } + this.target_.dispatchEvent(/** @type {Event} */ (e)); return goog.base(this, 'dispatchEvent', e); }; @@ -67,11 +74,6 @@ ol.event.Drag.prototype.doDrag = function(e, x, y, dragFromScroll) { /** @override */ ol.event.Drag.prototype.defaultAction = function(x, y) {}; -/** @inheritDoc */ -ol.event.Drag.prototype.getEventTypes = function() { - return ol.event.Drag.EventType; -}; - /** @inheritDoc */ ol.event.Drag.prototype.destroy = ol.event.Drag.prototype.dispose; diff --git a/src/ol/event/Events.js b/src/ol/event/Events.js index 24a645154b..d333176e1e 100644 --- a/src/ol/event/Events.js +++ b/src/ol/event/Events.js @@ -47,7 +47,7 @@ ol.event.isMultiTouch = function(evt) { * @constructor * @extends {goog.events.EventTarget} * @param {Object} object The object we are creating this instance for. - * @param {!EventTarget=} opt_element An optional element that we want to + * @param {!Element=} opt_element An optional element that we want to * listen to browser events on. * @param {boolean=} opt_includeXY Should the 'xy' property automatically be * created for browser pointer events? In general, this should be false. If @@ -71,7 +71,7 @@ ol.event.Events = function(object, opt_element, opt_includeXY, opt_sequences) { /** * @private - * @type {EventTarget} + * @type {Element} * The element that this instance listens to mouse events on. */ this.element_ = null; @@ -115,7 +115,7 @@ ol.event.Events.prototype.setIncludeXY = function(includeXY) { }; /** - * @return {EventTarget} The element that this instance currently + * @return {Element} The element that this instance currently * listens to browser events on. */ ol.event.Events.prototype.getElement = function() { @@ -126,18 +126,17 @@ ol.event.Events.prototype.getElement = function() { * Attach this instance to a DOM element. When called, all browser events fired * on the provided element will be relayed by this instance. * - * @param {EventTarget} element A DOM element to attach + * @param {Element} element A DOM element to attach * browser events to. If called without this argument, all browser events * will be detached from the element they are currently attached to. */ ol.event.Events.prototype.setElement = function(element) { - var types, t; + var types = goog.events.EventType, t; if (this.element_) { - types = this.getBrowserEventTypes(); for (t in types) { // register the event cross-browser goog.events.unlisten( - this.element_, types[t], this.handleBrowserEvent, false, this + this.element_, types[t], this.dispatchEvent, true, this ); } this.destroySequences(); @@ -145,25 +144,21 @@ ol.event.Events.prototype.setElement = function(element) { } this.element_ = element || null; if (goog.isDefAndNotNull(element)) { - this.createSequences(element); - types = this.getBrowserEventTypes(); + this.createSequences(); for (t in types) { // register the event cross-browser goog.events.listen( - element, types[t], this.handleBrowserEvent, false, this + element, types[t], this.dispatchEvent, true, this ); } } }; -/** - * @param {EventTarget} target - */ -ol.event.Events.prototype.createSequences = function(target) { +ol.event.Events.prototype.createSequences = function() { for (var i=0, ii=this.sequenceProviders_.length; i} - */ -ol.event.Events.prototype.getBrowserEventTypes = function() { - var types = {}; - goog.object.extend(types, goog.events.EventType); - for (var i=this.sequences_.length-1; i>=0; --i) { - goog.object.extend(types, this.sequences_[i].getEventTypes()); - } - return types; -}; - /** * Register a listener for an event. * @@ -257,39 +240,40 @@ ol.event.Events.prototype.triggerEvent = function(type, opt_evt) { }; /** - * Basically just a wrapper to the triggerEvent() function, but takes - * care to set a property 'xy' on the event with the current mouse position. + * Basically just a wrapper to the parent's dispatchEvent() function, but takes + * care to set a property 'xy' on the event with the current mouse position and + * normalize clientX and clientY for multi-touch events. * - * @param {Event} evt + * @param {Event} evt Event object. + * @return {boolean} If anyone called preventDefault on the event object (or + * if any of the handlers returns false this will also return false. */ -ol.event.Events.prototype.handleBrowserEvent = function(evt) { +ol.event.Events.prototype.dispatchEvent = function(evt) { var type = evt.type, listeners = goog.events.getListeners(this.element_, type, false) .concat(goog.events.getListeners(this.element_, type, true)); - if (!listeners || listeners.length === 0) { + if (listeners && listeners.length > 0) { // noone's listening, bail out - return; - } - // add clientX & clientY to all events - corresponds to average x, y - var touches = evt.touches; - if (touches && touches[0]) { - var x = 0; - var y = 0; - var num = touches.length; - var touch; - for (var i=0; i} element - */ -ol.event.ISequence.prototype.getEventTypes = function() {}; - /** * Destroys the sequence */ diff --git a/src/ol/geom/Geometry.js b/src/ol/geom/Geometry.js index b24e76d1ed..9378df6ebc 100644 --- a/src/ol/geom/Geometry.js +++ b/src/ol/geom/Geometry.js @@ -32,3 +32,12 @@ ol.geom.Geometry.prototype.setBounds = function(bounds) { this.bounds_ = bounds; return this; }; + +/** + * @returns ol.Loc + */ +ol.geom.Geometry.prototype.getCentroid = function() { + //FIXME: stub only to get popups working + return new ol.Loc(-76,45); +}; + diff --git a/test/api.html b/test/api.html index 7a70332a77..3cb67c3177 100644 --- a/test/api.html +++ b/test/api.html @@ -58,6 +58,7 @@ + +