From b097d260b87dd60174c107ee112ac1a7444d1a52 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Mon, 1 Jun 2015 16:42:33 +0200
Subject: [PATCH] no-jira: hotspot without applet

---
 .../components/AssessmentTestComponent.java   |   6 +-
 .../ui/rendering/AssessmentRenderer.java      |  56 ++-
 .../interactions/hotspotInteraction.xsl       | 103 +++--
 .../js/jquery/maphilight/jquery.maphilight.js | 368 ++++++++++++++++++
 .../maphilight/jquery.maphilight.min.js       |   1 +
 5 files changed, 486 insertions(+), 48 deletions(-)
 create mode 100644 src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.js
 create mode 100644 src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.min.js

diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java
index 281ffd9b8eb..f7d07209d2e 100644
--- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java
+++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java
@@ -24,6 +24,7 @@ import java.net.URI;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.AbstractComponent;
 import org.olat.core.gui.components.ComponentRenderer;
+import org.olat.core.gui.control.JSAndCSSAdder;
 import org.olat.core.gui.render.ValidationResult;
 import org.olat.ims.qti21.ui.CandidateSessionContext;
 
@@ -96,7 +97,10 @@ public class AssessmentTestComponent extends AbstractComponent {
 	@Override
 	public void validate(UserRequest ureq, ValidationResult vr) {
 		super.validate(ureq, vr);
-		vr.getJsAndCSSAdder().addRequiredStaticJsFile("assessment/rendering/javascript/QtiWorksRendering.js");
+
+		JSAndCSSAdder jsa = vr.getJsAndCSSAdder();
+		jsa.addRequiredStaticJsFile("assessment/rendering/javascript/QtiWorksRendering.js");
+		jsa.addRequiredStaticJsFile("js/jquery/maphilight/jquery.maphilight.js");
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/ims/qti21/ui/rendering/AssessmentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/rendering/AssessmentRenderer.java
index cba4348c9d1..971dad59d3b 100644
--- a/src/main/java/org/olat/ims/qti21/ui/rendering/AssessmentRenderer.java
+++ b/src/main/java/org/olat/ims/qti21/ui/rendering/AssessmentRenderer.java
@@ -54,6 +54,8 @@ import javax.xml.transform.stream.StreamResult;
 import org.olat.core.dispatcher.impl.StaticMediaDispatcher;
 import org.olat.core.gui.render.StringOutput;
 import org.olat.core.helpers.Settings;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.WebappHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.validation.BeanPropertyBindingResult;
@@ -73,6 +75,7 @@ import uk.ac.ed.ph.jqtiplus.state.TestSessionState;
 import uk.ac.ed.ph.jqtiplus.state.marshalling.ItemSessionStateXmlMarshaller;
 import uk.ac.ed.ph.jqtiplus.state.marshalling.TestSessionStateXmlMarshaller;
 import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ClassPathResourceLocator;
+import uk.ac.ed.ph.jqtiplus.xmlutils.locators.FileResourceLocator;
 import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator;
 import uk.ac.ed.ph.jqtiplus.xmlutils.xslt.SimpleXsltStylesheetCache;
 import uk.ac.ed.ph.jqtiplus.xmlutils.xslt.XsltStylesheetManager;
@@ -98,30 +101,47 @@ public class AssessmentRenderer {
 
     private static final Logger logger = LoggerFactory.getLogger(AssessmentRenderer.class);
 
-    private static final URI serializeXsltUri = URI.create("classpath:/rendering-xslt/serialize.xsl");
-    private static final URI ctopXsltUri = URI.create("classpath:/rendering-xslt/ctop.xsl");
-    private static final URI itemStandaloneXsltUri = URI.create("classpath:/rendering-xslt/item-standalone.xsl");
-    private static final URI testItemXsltUri = URI.create("classpath:/rendering-xslt/test-item.xsl");
-    private static final URI testEntryXsltUri = URI.create("classpath:/rendering-xslt/test-entry.xsl");
-    private static final URI testPartNavigationXsltUri = URI.create("classpath:/rendering-xslt/test-testpart-navigation.xsl");
-    private static final URI testPartFeedbackXsltUri = URI.create("classpath:/rendering-xslt/test-testpart-feedback.xsl");
-    private static final URI testFeedbackXsltUri = URI.create("classpath:/rendering-xslt/test-feedback.xsl");
-    private static final URI terminatedXsltUri = URI.create("classpath:/rendering-xslt/terminated.xsl");
-    private static final URI explodedXsltUri = URI.create("classpath:/rendering-xslt/exploded.xsl");
-
-
+    private static final URI serializeXsltUri;
+    private static final URI ctopXsltUri;
+    private static final URI itemStandaloneXsltUri;
+    private static final URI testItemXsltUri;
+    private static final URI testEntryXsltUri;
+    private static final URI testPartNavigationXsltUri;
+    private static final URI testPartFeedbackXsltUri;
+    private static final URI testFeedbackXsltUri;
+    private static final URI terminatedXsltUri;
+    private static final URI explodedXsltUri;
+	
+    static {
+    	String path;
+    	if(Settings.isDebuging() && StringHelper.containsNonWhitespace(WebappHelper.getSourcePath())) {
+    		path = "file://" + WebappHelper.getSourcePath().replace("/src/main/java", "/src/main/resources");
+    	} else {
+    		path = "classpath:";
+    	}
+
+		serializeXsltUri = URI.create(path + "/rendering-xslt/serialize.xsl");
+	    ctopXsltUri = URI.create(path + "/rendering-xslt/ctop.xsl");
+	    itemStandaloneXsltUri = URI.create(path + "/rendering-xslt/item-standalone.xsl");
+	    testItemXsltUri = URI.create(path + "/rendering-xslt/test-item.xsl");
+	    testEntryXsltUri = URI.create(path + "/rendering-xslt/test-entry.xsl");
+	    testPartNavigationXsltUri = URI.create(path + "/rendering-xslt/test-testpart-navigation.xsl");
+	    testPartFeedbackXsltUri = URI.create(path + "/rendering-xslt/test-testpart-feedback.xsl");
+	    testFeedbackXsltUri = URI.create(path + "/rendering-xslt/test-feedback.xsl");
+	    terminatedXsltUri = URI.create(path + "/rendering-xslt/terminated.xsl");
+	    explodedXsltUri = URI.create(path + "/rendering-xslt/exploded.xsl");
+    }
 
     /** Manager for the XSLT stylesheets, created during init. */
     private XsltStylesheetManager stylesheetManager;
 
-    //----------------------------------------------------
-
-
-    
-
     //----------------------------------------------------
     public AssessmentRenderer() {
-        this.stylesheetManager = new XsltStylesheetManager(new ClassPathResourceLocator(), new SimpleXsltStylesheetCache());
+    	if(Settings.isDebuging() && StringHelper.containsNonWhitespace(WebappHelper.getSourcePath())) {
+    		stylesheetManager = new XsltStylesheetManager(new FileResourceLocator(), null);
+    	} else {
+    		stylesheetManager = new XsltStylesheetManager(new ClassPathResourceLocator(), new SimpleXsltStylesheetCache());
+    	}
     }
 
     //----------------------------------------------------
diff --git a/src/main/resources/rendering-xslt/interactions/hotspotInteraction.xsl b/src/main/resources/rendering-xslt/interactions/hotspotInteraction.xsl
index 4eba570ed5f..736461ba9df 100644
--- a/src/main/resources/rendering-xslt/interactions/hotspotInteraction.xsl
+++ b/src/main/resources/rendering-xslt/interactions/hotspotInteraction.xsl
@@ -22,37 +22,82 @@
 
       <xsl:variable name="object" select="qti:object" as="element(qti:object)"/>
       <xsl:variable name="appletContainerId" select="concat('qtiworks_id_appletContainer_', @responseIdentifier)" as="xs:string"/>
+      <xsl:variable name="responseValue" select="qw:get-response-value(/, @responseIdentifier)" as="element(qw:responseVariable)?"/>
+      
       <div id="{$appletContainerId}" class="appletContainer v2">
-        <object type="application/x-java-applet" height="{$object/@height + 40}" width="{$object/@width}">
-          <param name="code" value="BoundedGraphicalApplet"/>
-          <param name="codebase" value="{$appletCodebase}"/>
-          <param name="identifier" value="{@responseIdentifier}"/>
-          <param name="operation_mode" value="hotspot_interaction"/>
-          <!-- (BoundedGraphicalApplet uses -1 to represent 'unlimited') -->
-          <param name="number_of_responses" value="{if (@maxChoices &gt; 0) then @maxChoices else -1}"/>
-          <param name="background_image" value="{qw:convert-link-full($object/@data)}"/>
-          <xsl:variable name="hotspotChoices" select="qw:filter-visible(qti:hotspotChoice)" as="element(qti:hotspotChoice)*"/>
-          <param name="hotspot_count" value="{count($hotspotChoices)}"/>
-          <xsl:for-each select="qti:hotspotChoice">
-            <param name="hotspot{position()-1}"
-              value="{@identifier}::::{@shape}::{@coords}{if (@label) then concat('::hotSpotLabel',@label) else ''}{if (@matchGroup) then concat('::', translate(normalize-space(@matchGroup), ' ', '::')) else ''}"/>
-          </xsl:for-each>
+        <img id="{$appletContainerId}_img" width="206" height="280" src="{qw:convert-link-full($object/@data)}" usemap="#{$appletContainerId}_map"></img>
+        <map name="{$appletContainerId}_map">
+        	<xsl:for-each select="qti:hotspotChoice">
+            	<!-- Match group, label -->
+          		<area id="{@identifier}" shape="{@shape}" coords="{@coords}" href="javascript:clickArea('{@identifier}')" data-maphilight=''></area>
+          	</xsl:for-each>
+		</map>
 
-          <xsl:variable name="responseValue" select="qw:get-response-value(/, @responseIdentifier)" as="element(qw:responseVariable)?"/>
-          <xsl:if test="qw:is-not-null-value($responseValue)">
-            <param name="feedback">
-              <xsl:attribute name="value">
-                <xsl:value-of select="$responseValue/qw:value" separator=","/>
-              </xsl:attribute>
-            </param>
-          </xsl:if>
-        </object>
-        <script type="text/javascript">
-          jQuery(document).ready(function() {
-            QtiWorksRendering.registerAppletBasedInteractionContainer('<xsl:value-of
-              select="$appletContainerId"/>', ['<xsl:value-of select="@responseIdentifier"/>']);
-          });
-        </script>
+		<script type="text/javascript">
+			jQuery(function() {
+				jQuery('#<xsl:value-of select="$appletContainerId"/>_img').maphilight({
+					fillColor: '888888',
+					strokeColor: '0000ff',
+					strokeWidth: 3
+				});
+			});
+			
+			<xsl:choose>
+				<xsl:when test="qw:is-not-null-value($responseValue)">
+
+			jQuery(function() {
+				var areaIds = '<xsl:value-of select="$responseValue/qw:value" separator=","/>'.split(',');
+				for(i=areaIds.length; i-->0; ) {
+					var areaEl = jQuery('#' + areaIds[i])
+					var data = areaEl.data('maphilight') || {};
+					data.alwaysOn = true;
+					areaEl.data('maphilight', data).trigger('alwaysOn.maphilight');
+				}
+			});
+			
+			function clickArea(spot) { };
+
+		        </xsl:when><xsl:otherwise>
+		        
+			function clickArea(spot) {
+				var areaEl = jQuery('#' + spot)
+				var data = areaEl.data('maphilight') || {};
+				if(!data.alwaysOn) {
+					var numOfChoices = 1;
+					if(numOfChoices > 0) {
+						var countChoices = 0;
+						jQuery("area", "map[name='<xsl:value-of select="$appletContainerId"/>_map']").each(function(index, el) {
+							var cData = jQuery(el).data('maphilight') || {};
+							if(cData.alwaysOn) {
+								countChoices++;
+								
+							}
+						});
+						if(countChoices >= numOfChoices) {
+							return false;
+						}
+					}
+				}
+            	data.alwaysOn = !data.alwaysOn;
+				areaEl.data('maphilight', data).trigger('alwaysOn.maphilight');
+
+				var divContainer = jQuery('#<xsl:value-of select="$appletContainerId"/>');
+				divContainer.find("input[type='hidden']").remove();
+				jQuery("area", "map[name='<xsl:value-of select="$appletContainerId"/>_map']").each(function(index, el) {
+					var cAreaEl = jQuery(el);
+					var cData = cAreaEl.data('maphilight') || {};
+					if(cData.alwaysOn) {
+						var inputElement = jQuery('<input type="hidden"/>')
+							.attr('name', 'qtiworks_response_<xsl:value-of select="@responseIdentifier"/>')
+							.attr('value', areaEl.attr('id'));
+						divContainer.append(inputElement);
+					}
+				});
+			};
+		        
+		        </xsl:otherwise>
+			</xsl:choose>
+		</script>
       </div>
     </div>
   </xsl:template>
diff --git a/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.js b/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.js
new file mode 100644
index 00000000000..c87dcbc42a3
--- /dev/null
+++ b/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.js
@@ -0,0 +1,368 @@
+(function($) {
+	var has_VML, has_canvas, create_canvas_for, add_shape_to, clear_canvas, shape_from_area,
+		canvas_style, hex_to_decimal, css3color, is_image_loaded, options_from_area;
+
+	has_canvas = !!document.createElement('canvas').getContext;
+
+	// VML: more complex
+	has_VML = (function() {
+		var a = document.createElement('div');
+		a.innerHTML = '<v:shape id="vml_flag1" adj="1" />';
+		var b = a.firstChild;
+		b.style.behavior = "url(#default#VML)";
+		return b ? typeof b.adj == "object": true;
+	})();
+
+	if(!(has_canvas || has_VML)) {
+		$.fn.maphilight = function() { return this; };
+		return;
+	}
+	
+	if(has_canvas) {
+		hex_to_decimal = function(hex) {
+			return Math.max(0, Math.min(parseInt(hex, 16), 255));
+		};
+		css3color = function(color, opacity) {
+			return 'rgba('+hex_to_decimal(color.substr(0,2))+','+hex_to_decimal(color.substr(2,2))+','+hex_to_decimal(color.substr(4,2))+','+opacity+')';
+		};
+		create_canvas_for = function(img) {
+			var c = $('<canvas style="width:'+$(img).width()+'px;height:'+$(img).height()+'px;"></canvas>').get(0);
+			c.getContext("2d").clearRect(0, 0, $(img).width(), $(img).height());
+			return c;
+		};
+		var draw_shape = function(context, shape, coords, x_shift, y_shift) {
+			x_shift = x_shift || 0;
+			y_shift = y_shift || 0;
+			
+			context.beginPath();
+			if(shape == 'rect') {
+				// x, y, width, height
+				context.rect(coords[0] + x_shift, coords[1] + y_shift, coords[2] - coords[0], coords[3] - coords[1]);
+			} else if(shape == 'poly') {
+				context.moveTo(coords[0] + x_shift, coords[1] + y_shift);
+				for(i=2; i < coords.length; i+=2) {
+					context.lineTo(coords[i] + x_shift, coords[i+1] + y_shift);
+				}
+			} else if(shape == 'circ') {
+				// x, y, radius, startAngle, endAngle, anticlockwise
+				context.arc(coords[0] + x_shift, coords[1] + y_shift, coords[2], 0, Math.PI * 2, false);
+			}
+			context.closePath();
+		}
+		add_shape_to = function(canvas, shape, coords, options, name) {
+			var i, context = canvas.getContext('2d');
+			
+			// Because I don't want to worry about setting things back to a base state
+			
+			// Shadow has to happen first, since it's on the bottom, and it does some clip /
+			// fill operations which would interfere with what comes next.
+			if(options.shadow) {
+				context.save();
+				if(options.shadowPosition == "inside") {
+					// Cause the following stroke to only apply to the inside of the path
+					draw_shape(context, shape, coords);
+					context.clip();
+				}
+				
+				// Redraw the shape shifted off the canvas massively so we can cast a shadow
+				// onto the canvas without having to worry about the stroke or fill (which
+				// cannot have 0 opacity or width, since they're what cast the shadow).
+				var x_shift = canvas.width * 100;
+				var y_shift = canvas.height * 100;
+				draw_shape(context, shape, coords, x_shift, y_shift);
+				
+				context.shadowOffsetX = options.shadowX - x_shift;
+				context.shadowOffsetY = options.shadowY - y_shift;
+				context.shadowBlur = options.shadowRadius;
+				context.shadowColor = css3color(options.shadowColor, options.shadowOpacity);
+				
+				// Now, work out where to cast the shadow from! It looks better if it's cast
+				// from a fill when it's an outside shadow or a stroke when it's an interior
+				// shadow. Allow the user to override this if they need to.
+				var shadowFrom = options.shadowFrom;
+				if (!shadowFrom) {
+					if (options.shadowPosition == 'outside') {
+						shadowFrom = 'fill';
+					} else {
+						shadowFrom = 'stroke';
+					}
+				}
+				if (shadowFrom == 'stroke') {
+					context.strokeStyle = "rgba(0,0,0,1)";
+					context.stroke();
+				} else if (shadowFrom == 'fill') {
+					context.fillStyle = "rgba(0,0,0,1)";
+					context.fill();
+				}
+				context.restore();
+				
+				// and now we clean up
+				if(options.shadowPosition == "outside") {
+					context.save();
+					// Clear out the center
+					draw_shape(context, shape, coords);
+					context.globalCompositeOperation = "destination-out";
+					context.fillStyle = "rgba(0,0,0,1);";
+					context.fill();
+					context.restore();
+				}
+			}
+			
+			context.save();
+			
+			draw_shape(context, shape, coords);
+			
+			// fill has to come after shadow, otherwise the shadow will be drawn over the fill,
+			// which mostly looks weird when the shadow has a high opacity
+			if(options.fill) {
+				context.fillStyle = css3color(options.fillColor, options.fillOpacity);
+				context.fill();
+			}
+			// Likewise, stroke has to come at the very end, or it'll wind up under bits of the
+			// shadow or the shadow-background if it's present.
+			if(options.stroke) {
+				context.strokeStyle = css3color(options.strokeColor, options.strokeOpacity);
+				context.lineWidth = options.strokeWidth;
+				context.stroke();
+			}
+			
+			context.restore();
+			
+			if(options.fade) {
+				$(canvas).css('opacity', 0).animate({opacity: 1}, 100);
+			}
+		};
+		clear_canvas = function(canvas) {
+			canvas.getContext('2d').clearRect(0, 0, canvas.width,canvas.height);
+		};
+	} else {   // ie executes this code
+		create_canvas_for = function(img) {
+			return $('<var style="zoom:1;overflow:hidden;display:block;width:'+img.width+'px;height:'+img.height+'px;"></var>').get(0);
+		};
+		add_shape_to = function(canvas, shape, coords, options, name) {
+			var fill, stroke, opacity, e;
+			for (var i in coords) { coords[i] = parseInt(coords[i], 10); }
+			fill = '<v:fill color="#'+options.fillColor+'" opacity="'+(options.fill ? options.fillOpacity : 0)+'" />';
+			stroke = (options.stroke ? 'strokeweight="'+options.strokeWidth+'" stroked="t" strokecolor="#'+options.strokeColor+'"' : 'stroked="f"');
+			opacity = '<v:stroke opacity="'+options.strokeOpacity+'"/>';
+			if(shape == 'rect') {
+				e = $('<v:rect name="'+name+'" filled="t" '+stroke+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+coords[0]+'px;top:'+coords[1]+'px;width:'+(coords[2] - coords[0])+'px;height:'+(coords[3] - coords[1])+'px;"></v:rect>');
+			} else if(shape == 'poly') {
+				e = $('<v:shape name="'+name+'" filled="t" '+stroke+' coordorigin="0,0" coordsize="'+canvas.width+','+canvas.height+'" path="m '+coords[0]+','+coords[1]+' l '+coords.join(',')+' x e" style="zoom:1;margin:0;padding:0;display:block;position:absolute;top:0px;left:0px;width:'+canvas.width+'px;height:'+canvas.height+'px;"></v:shape>');
+			} else if(shape == 'circ') {
+				e = $('<v:oval name="'+name+'" filled="t" '+stroke+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+(coords[0] - coords[2])+'px;top:'+(coords[1] - coords[2])+'px;width:'+(coords[2]*2)+'px;height:'+(coords[2]*2)+'px;"></v:oval>');
+			}
+			e.get(0).innerHTML = fill+opacity;
+			$(canvas).append(e);
+		};
+		clear_canvas = function(canvas) {
+			// jquery1.8 + ie7 
+			var $html = $("<div>" + canvas.innerHTML + "</div>");
+			$html.children('[name=highlighted]').remove();
+			canvas.innerHTML = $html.html();
+		};
+	}
+	
+	shape_from_area = function(area) {
+		var i, coords = area.getAttribute('coords').split(',');
+		for (i=0; i < coords.length; i++) { coords[i] = parseFloat(coords[i]); }
+		return [area.getAttribute('shape').toLowerCase().substr(0,4), coords];
+	};
+
+	options_from_area = function(area, options) {
+		var $area = $(area);
+		return $.extend({}, options, $.metadata ? $area.metadata() : false, $area.data('maphilight'));
+	};
+	
+	is_image_loaded = function(img) {
+		if(!img.complete) { return false; } // IE
+		if(typeof img.naturalWidth != "undefined" && img.naturalWidth === 0) { return false; } // Others
+		return true;
+	};
+
+	canvas_style = {
+		position: 'absolute',
+		left: 0,
+		top: 0,
+		padding: 0,
+		border: 0
+	};
+	
+	var ie_hax_done = false;
+	$.fn.maphilight = function(opts) {
+		opts = $.extend({}, $.fn.maphilight.defaults, opts);
+		
+		if(!has_canvas && !ie_hax_done) {
+			$(window).ready(function() {
+				document.namespaces.add("v", "urn:schemas-microsoft-com:vml");
+				var style = document.createStyleSheet();
+				var shapes = ['shape','rect', 'oval', 'circ', 'fill', 'stroke', 'imagedata', 'group','textbox'];
+				$.each(shapes,
+					function() {
+						style.addRule('v\\:' + this, "behavior: url(#default#VML); antialias:true");
+					}
+				);
+			});
+			ie_hax_done = true;
+		}
+		
+		return this.each(function() {
+			var img, wrap, options, map, canvas, canvas_always, mouseover, highlighted_shape, usemap;
+			img = $(this);
+
+			if(!is_image_loaded(this)) {
+				// If the image isn't fully loaded, this won't work right.  Try again later.
+				return window.setTimeout(function() {
+					img.maphilight(opts);
+				}, 200);
+			}
+
+			options = $.extend({}, opts, $.metadata ? img.metadata() : false, img.data('maphilight'));
+
+			// jQuery bug with Opera, results in full-url#usemap being returned from jQuery's attr.
+			// So use raw getAttribute instead.
+			usemap = img.get(0).getAttribute('usemap');
+
+			if (!usemap) {
+				return
+			}
+
+			map = $('map[name="'+usemap.substr(1)+'"]');
+
+			if(!(img.is('img,input[type="image"]') && usemap && map.size() > 0)) {
+				return;
+			}
+
+			if(img.hasClass('maphilighted')) {
+				// We're redrawing an old map, probably to pick up changes to the options.
+				// Just clear out all the old stuff.
+				var wrapper = img.parent();
+				img.insertBefore(wrapper);
+				wrapper.remove();
+				$(map).unbind('.maphilight').find('area[coords]').unbind('.maphilight');
+			}
+
+			wrap = $('<div></div>').css({
+				display:'block',
+				background:'url("'+this.src+'")',
+				position:'relative',
+				padding:0,
+				width:this.width,
+				height:this.height
+				});
+			if(options.wrapClass) {
+				if(options.wrapClass === true) {
+					wrap.addClass($(this).attr('class'));
+				} else {
+					wrap.addClass(options.wrapClass);
+				}
+			}
+			img.before(wrap).css('opacity', 0).css(canvas_style).remove();
+			if(has_VML) { img.css('filter', 'Alpha(opacity=0)'); }
+			wrap.append(img);
+			
+			canvas = create_canvas_for(this);
+			$(canvas).css(canvas_style);
+			canvas.height = this.height;
+			canvas.width = this.width;
+			
+			mouseover = function(e) {
+				var shape, area_options;
+				area_options = options_from_area(this, options);
+				if(
+					!area_options.neverOn
+					&&
+					!area_options.alwaysOn
+				) {
+					shape = shape_from_area(this);
+					add_shape_to(canvas, shape[0], shape[1], area_options, "highlighted");
+					if(area_options.groupBy) {
+						var areas;
+						// two ways groupBy might work; attribute and selector
+						if(/^[a-zA-Z][\-a-zA-Z]+$/.test(area_options.groupBy)) {
+							areas = map.find('area['+area_options.groupBy+'="'+$(this).attr(area_options.groupBy)+'"]');
+						} else {
+							areas = map.find(area_options.groupBy);
+						}
+						var first = this;
+						areas.each(function() {
+							if(this != first) {
+								var subarea_options = options_from_area(this, options);
+								if(!subarea_options.neverOn && !subarea_options.alwaysOn) {
+									var shape = shape_from_area(this);
+									add_shape_to(canvas, shape[0], shape[1], subarea_options, "highlighted");
+								}
+							}
+						});
+					}
+					// workaround for IE7, IE8 not rendering the final rectangle in a group
+					if(!has_canvas) {
+						$(canvas).append('<v:rect></v:rect>');
+					}
+				}
+			}
+
+			$(map).bind('alwaysOn.maphilight', function() {
+				// Check for areas with alwaysOn set. These are added to a *second* canvas,
+				// which will get around flickering during fading.
+				if(canvas_always) {
+					clear_canvas(canvas_always);
+				}
+				if(!has_canvas) {
+					$(canvas).empty();
+				}
+				$(map).find('area[coords]').each(function() {
+					var shape, area_options;
+					area_options = options_from_area(this, options);
+					if(area_options.alwaysOn) {
+						if(!canvas_always && has_canvas) {
+							canvas_always = create_canvas_for(img[0]);
+							$(canvas_always).css(canvas_style);
+							canvas_always.width = img[0].width;
+							canvas_always.height = img[0].height;
+							img.before(canvas_always);
+						}
+						area_options.fade = area_options.alwaysOnFade; // alwaysOn shouldn't fade in initially
+						shape = shape_from_area(this);
+						if (has_canvas) {
+							add_shape_to(canvas_always, shape[0], shape[1], area_options, "");
+						} else {
+							add_shape_to(canvas, shape[0], shape[1], area_options, "");
+						}
+					}
+				});
+			});
+			
+			$(map).trigger('alwaysOn.maphilight').find('area[coords]')
+				.bind('mouseover.maphilight', mouseover)
+				.bind('mouseout.maphilight', function(e) { clear_canvas(canvas); });
+			
+			img.before(canvas); // if we put this after, the mouseover events wouldn't fire.
+			
+			img.addClass('maphilighted');
+		});
+	};
+	$.fn.maphilight.defaults = {
+		fill: true,
+		fillColor: '000000',
+		fillOpacity: 0.2,
+		stroke: true,
+		strokeColor: 'ff0000',
+		strokeOpacity: 1,
+		strokeWidth: 1,
+		fade: true,
+		alwaysOn: false,
+		neverOn: false,
+		groupBy: false,
+		wrapClass: true,
+		// plenty of shadow:
+		shadow: false,
+		shadowX: 0,
+		shadowY: 0,
+		shadowRadius: 6,
+		shadowColor: '000000',
+		shadowOpacity: 0.8,
+		shadowPosition: 'outside',
+		shadowFrom: false
+	};
+})(jQuery);
diff --git a/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.min.js b/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.min.js
new file mode 100644
index 00000000000..ec9d0214fd4
--- /dev/null
+++ b/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.min.js
@@ -0,0 +1 @@
+(function(G){var B,J,C,K,N,M,I,E,H,A,L;J=!!document.createElement("canvas").getContext;B=(function(){var P=document.createElement("div");P.innerHTML='<v:shape id="vml_flag1" adj="1" />';var O=P.firstChild;O.style.behavior="url(#default#VML)";return O?typeof O.adj=="object":true})();if(!(J||B)){G.fn.maphilight=function(){return this};return }if(J){E=function(O){return Math.max(0,Math.min(parseInt(O,16),255))};H=function(O,P){return"rgba("+E(O.substr(0,2))+","+E(O.substr(2,2))+","+E(O.substr(4,2))+","+P+")"};C=function(O){var P=G('<canvas style="width:'+O.width+"px;height:"+O.height+'px;"></canvas>').get(0);P.getContext("2d").clearRect(0,0,P.width,P.height);return P};var F=function(Q,O,R,P,S){P=P||0;S=S||0;Q.beginPath();if(O=="rect"){Q.rect(R[0]+P,R[1]+S,R[2]-R[0],R[3]-R[1])}else{if(O=="poly"){Q.moveTo(R[0]+P,R[1]+S);for(i=2;i<R.length;i+=2){Q.lineTo(R[i]+P,R[i+1]+S)}}else{if(O=="circ"){Q.arc(R[0]+P,R[1]+S,R[2],0,Math.PI*2,false)}}}Q.closePath()};K=function(Q,T,U,X,O){var S,P=Q.getContext("2d");if(X.shadow){P.save();if(X.shadowPosition=="inside"){F(P,T,U);P.clip()}var R=Q.width*100;var W=Q.height*100;F(P,T,U,R,W);P.shadowOffsetX=X.shadowX-R;P.shadowOffsetY=X.shadowY-W;P.shadowBlur=X.shadowRadius;P.shadowColor=H(X.shadowColor,X.shadowOpacity);var V=X.shadowFrom;if(!V){if(X.shadowPosition=="outside"){V="fill"}else{V="stroke"}}if(V=="stroke"){P.strokeStyle="rgba(0,0,0,1)";P.stroke()}else{if(V=="fill"){P.fillStyle="rgba(0,0,0,1)";P.fill()}}P.restore();if(X.shadowPosition=="outside"){P.save();F(P,T,U);P.globalCompositeOperation="destination-out";P.fillStyle="rgba(0,0,0,1);";P.fill();P.restore()}}P.save();F(P,T,U);if(X.fill){P.fillStyle=H(X.fillColor,X.fillOpacity);P.fill()}if(X.stroke){P.strokeStyle=H(X.strokeColor,X.strokeOpacity);P.lineWidth=X.strokeWidth;P.stroke()}P.restore();if(X.fade){G(Q).css("opacity",0).animate({opacity:1},100)}};N=function(O){O.getContext("2d").clearRect(0,0,O.width,O.height)}}else{C=function(O){return G('<var style="zoom:1;overflow:hidden;display:block;width:'+O.width+"px;height:"+O.height+'px;"></var>').get(0)};K=function(P,T,U,X,O){var V,W,R,S;for(var Q in U){U[Q]=parseInt(U[Q],10)}V='<v:fill color="#'+X.fillColor+'" opacity="'+(X.fill?X.fillOpacity:0)+'" />';W=(X.stroke?'strokeweight="'+X.strokeWidth+'" stroked="t" strokecolor="#'+X.strokeColor+'"':'stroked="f"');R='<v:stroke opacity="'+X.strokeOpacity+'"/>';if(T=="rect"){S=G('<v:rect name="'+O+'" filled="t" '+W+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+U[0]+"px;top:"+U[1]+"px;width:"+(U[2]-U[0])+"px;height:"+(U[3]-U[1])+'px;"></v:rect>')}else{if(T=="poly"){S=G('<v:shape name="'+O+'" filled="t" '+W+' coordorigin="0,0" coordsize="'+P.width+","+P.height+'" path="m '+U[0]+","+U[1]+" l "+U.join(",")+' x e" style="zoom:1;margin:0;padding:0;display:block;position:absolute;top:0px;left:0px;width:'+P.width+"px;height:"+P.height+'px;"></v:shape>')}else{if(T=="circ"){S=G('<v:oval name="'+O+'" filled="t" '+W+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+(U[0]-U[2])+"px;top:"+(U[1]-U[2])+"px;width:"+(U[2]*2)+"px;height:"+(U[2]*2)+'px;"></v:oval>')}}}S.get(0).innerHTML=V+R;G(P).append(S)};N=function(P){var O=G("<div>"+P.innerHTML+"</div>");O.children("[name=highlighted]").remove();P.innerHTML=O.html()}}M=function(P){var O,Q=P.getAttribute("coords").split(",");for(O=0;O<Q.length;O++){Q[O]=parseFloat(Q[O])}return[P.getAttribute("shape").toLowerCase().substr(0,4),Q]};L=function(Q,P){var O=G(Q);return G.extend({},P,G.metadata?O.metadata():false,O.data("maphilight"))};A=function(O){if(!O.complete){return false}if(typeof O.naturalWidth!="undefined"&&O.naturalWidth===0){return false}return true};I={position:"absolute",left:0,top:0,padding:0,border:0};var D=false;G.fn.maphilight=function(O){O=G.extend({},G.fn.maphilight.defaults,O);if(!J&&!D){G(window).ready(function(){document.namespaces.add("v","urn:schemas-microsoft-com:vml");var Q=document.createStyleSheet();var P=["shape","rect","oval","circ","fill","stroke","imagedata","group","textbox"];G.each(P,function(){Q.addRule("v\\:"+this,"behavior: url(#default#VML); antialias:true")})});D=true}return this.each(function(){var U,R,Y,Q,T,V,X,S,W;U=G(this);if(!A(this)){return window.setTimeout(function(){U.maphilight(O)},200)}Y=G.extend({},O,G.metadata?U.metadata():false,U.data("maphilight"));W=U.get(0).getAttribute("usemap");if(!W){return }Q=G('map[name="'+W.substr(1)+'"]');if(!(U.is('img,input[type="image"]')&&W&&Q.size()>0)){return }if(U.hasClass("maphilighted")){var P=U.parent();U.insertBefore(P);P.remove();G(Q).unbind(".maphilight").find("area[coords]").unbind(".maphilight")}R=G("<div></div>").css({display:"block",background:'url("'+this.src+'")',position:"relative",padding:0,width:this.width,height:this.height});if(Y.wrapClass){if(Y.wrapClass===true){R.addClass(G(this).attr("class"))}else{R.addClass(Y.wrapClass)}}U.before(R).css("opacity",0).css(I).remove();if(B){U.css("filter","Alpha(opacity=0)")}R.append(U);T=C(this);G(T).css(I);T.height=this.height;T.width=this.width;X=function(c){var a,b;b=L(this,Y);if(!b.neverOn&&!b.alwaysOn){a=M(this);K(T,a[0],a[1],b,"highlighted");if(b.groupBy){var Z;if(/^[a-zA-Z][\-a-zA-Z]+$/.test(b.groupBy)){Z=Q.find("area["+b.groupBy+'="'+G(this).attr(b.groupBy)+'"]')}else{Z=Q.find(b.groupBy)}var d=this;Z.each(function(){if(this!=d){var f=L(this,Y);if(!f.neverOn&&!f.alwaysOn){var e=M(this);K(T,e[0],e[1],f,"highlighted")}}})}if(!J){G(T).append("<v:rect></v:rect>")}}};G(Q).bind("alwaysOn.maphilight",function(){if(V){N(V)}if(!J){G(T).empty()}G(Q).find("area[coords]").each(function(){var Z,a;a=L(this,Y);if(a.alwaysOn){if(!V&&J){V=C(U[0]);G(V).css(I);V.width=U[0].width;V.height=U[0].height;U.before(V)}a.fade=a.alwaysOnFade;Z=M(this);if(J){K(V,Z[0],Z[1],a,"")}else{K(T,Z[0],Z[1],a,"")}}})});G(Q).trigger("alwaysOn.maphilight").find("area[coords]").bind("mouseover.maphilight",X).bind("mouseout.maphilight",function(Z){N(T)});U.before(T);U.addClass("maphilighted")})};G.fn.maphilight.defaults={fill:true,fillColor:"000000",fillOpacity:0.2,stroke:true,strokeColor:"ff0000",strokeOpacity:1,strokeWidth:1,fade:true,alwaysOn:false,neverOn:false,groupBy:false,wrapClass:true,shadow:false,shadowX:0,shadowY:0,shadowRadius:6,shadowColor:"000000",shadowOpacity:0.8,shadowPosition:"outside",shadowFrom:false}})(jQuery);
\ No newline at end of file
-- 
GitLab