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/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/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 @@
+
+