Source: ol/featuretip.js maps.nyc.gov

var nyc = nyc || {};
nyc.ol = nyc.ol || {};

/**
 * @desc A class for providing tool tips on mouseover for vector features
 * @public
 * @class
 * @constructor
 * @param {ol.Map} map The OpenLayers map with which the user will interact
 * @param {Array<nyc.ol.FeatureTip.TipDef>=} tipDefs The tip definitions
 */
nyc.ol.FeatureTip = function(map, tipDefs){
	this.map = map;
	this.mapDiv = map.getTarget();
	if (tipDefs) this.addtipDefs(tipDefs);
	if (!$(this.mapDiv).children('.feature-tip').length)
		$(this.mapDiv).append('<div class="feature-tip" role="tooltip"></div><div class="feature-tip-helper" style="left:-1000px;top:-1000px"></div>');
	this.tip = $(this.mapDiv).children('.feature-tip');
	this.helper = $(this.mapDiv).children('.feature-tip-helper');
	$(map.getViewport()).mousemove($.proxy(this.label, this));
};

nyc.ol.FeatureTip.prototype = {
	/** 
	 * @private
	 * @member {ol.Map}
	 */
	map: null,
	/** 
	 * @private
	 * @member {Element}
	 */
	mapDiv: null,
	/** 
	 * @private
	 * @member {JQuery}
	 */
	tip: null,
	/** 
	 * @private
	 * @member {JQuery}
	 */
	helper: null,
	/**
	 * @desc Hide the feature tip
	 * @public
	 * @method
	 */
	hide: function(){
		this.tip.fadeOut();
	},
	/**
	 * @desc Adds tip definitions
	 * @public
	 * @method
	 * @param {Array<nyc.ol.FeatureTip.TipDef>} tipDefs The tip definitions to add
	 */
	addtipDefs: function(tipDefs){
		$.each(tipDefs, function(_, def){
			var src = def.source, func = def.labelFunction, features = src.getFeatures();
			if (features.getArray) features = features.getArray();
			$.each(features, function(_, f){
				f.getLabel = func;
			});
			src.on('addfeature', function(e){
				e.feature.getLabel = func;
			});
		});		
	},
	/**
	 * @private
	 * @method
	 * @param {ol.source.VectorEvent} event
	 */
	label: function(event){
			var px = this.map.getEventPixel(event.originalEvent),
				lbl = this.map.forEachFeatureAtPixel(px, function(f, _){
					return f && f.getLabel ? f.getLabel() : null;
				});
		if (lbl){
			this.tip.html(lbl.text)
				.css({left: (px[0] + 10) + 'px', top: (px[1] + 10) + 'px'})
				.show();
			this.tip[0].className = 'feature-tip ' + (lbl.cssClass || '');
			this.flip(lbl, px);
		}else{
			this.tip.fadeOut();
		}
	},
	/**
	 * @private
	 * @param {nyc.ol.FeatureLabel} lbl
	 * @param {ol.Pixel} px 
	 */
	flip: function(lbl, px){
		var width = this.helper.html(lbl.text).width(), height = this.helper.height();
		this.helper[0].className = 'feature-tip-helper ' + (lbl.cssClass || '');
		if ((this.tip.position().left + width) > $(this.mapDiv).width()){
			this.tip.css('left', px[0] - width - 10 + 'px');
		}		
		if ((this.tip.position().top + height) > $(this.mapDiv).height()){
			this.tip.css('top', px[1] - height - 10 + 'px');
		}		
	}	
};

/**
 * @desc Object with configuration options for feature tips 
 * @public
 * @typedef {Object}
 * @property {ol.source.Vector} source The source of the features
 * @property {nyc.ol.FeatureTip.LabelFunction} labelFunction A function to generate tips
 */
nyc.ol.FeatureTip.TipDef;

/**
 * @desc Label function that runs in the scope of the feature and returns a {@link nyc.ol.FeatureTip.Label}
 * @public
 * @typedef {function():nyc.ol.FeatureTip.Label}
 */
nyc.ol.FeatureTip.LabelFunction;

/**
 * @desc Object type to return from a feature's label function
 * @public
 * @typedef {Object}
 * @property {string} text The tip text
 * @property {string=} cssClass A CSS class to apply to the tip 
 */
nyc.ol.FeatureTip.Label;