Summary: imagemap.js is a ‘reactive’ javascript alternative to HTML image maps (i.e. the user is shown what he or she is getting).

doc : source : index

HTML header : HTML body : additional javascript : map structure : region structure : style structures : code for image generation : creating a mapped image in javascript : hints : map object fields overwritten by imagemap : contact me : usage rights

North Italy
The image on the right illustrates the use of imagemap: hover over the coloured routes to see how it responds. Clicking in one of these areas takes you to a region-specific link, whereas clicking elsewhere takes you to the main image link. The title tells you where you are going. (The URL line on the bottom-left of your browser window may give you the full location. In Chrome it does so; Firefox has a minor bug in that it doesn’t update the display when the URL is changed by the script.)

This page documents imagemap by showing how to produce the image using it. The main account applies to images in HTML pages, requiring the addition of a little javascript. A further paragraph explains how to invoke imagemap from pure javascript pages. The source code for this page itself is an example of using imagemap in HTML; our Cordillera Blanca intro is slightly simpler in not having a title and in the image not being a link. Our home page is a fairly elaborate piece of javascript using imagemap.

There are probably hundreds of similar modules all round the web. Google doesn’t find them for me. The only one I know of is the jQuery-based imagemapster, whose aims are rather different.

imagemap doesn’t require a state-of-the-art browser or any external libraries. It works correctly for me on a Mac which claims to have been built in ‘Late 2006’.

A couple of lines in the HTML header provide relevant definitions. Firstly we load the script by

<script src="https://www.masterlyinactivity.com/imagemap.js"></script>

You can do exactly this, but it is better to make a local copy of imagemap to avoid an unnecessary external dependence.

And secondly, imagemap images are often links, but can only be seen as links if they have borders, which need to be requested in some way. On this page I define a ‘bordered’ class in the initial style definition:

img.bordered{border:1px solid;padding:2px}

An earlier version of imagemap required the image to be a link. This is no longer necessary. It works almost perfectly for images which are not links (treating them as empty links).

At the point where the image is needed, you do not supply the image itself but rather a ‘div’ which will be overwritten by it.

<p><table cellpadding=0 cellspacing=0 style="float:right;margin:6px 0 6px 6px">
<tr><td align=left><b id=imghead>North Italy</b>
<tr><td><div id=imgdiv style="width:180px;height:106px;background-color:grey"></div></table>

This is standard HTML. Notice that we supply an ‘id’ for both the div and the element containing the header text. These will be used to tell the javascript which elements to operate on. We also supply a size and background colour for the div. These aren’t essential. The size saves the browser from having to adjust the layout when the div is overwritten. The grey background avoids leaving a suspicious blank until the image has been loaded.

The size is not the size of the image, but the size of a minimal container capable of holding it, making allowance for the border, padding and margin. In this case the border is 1 pixel and the padding 2, so the div is 6 pixels wider and higher than the image itself.

Finally, at the end of the page after the ‘</body>’ and before the ‘</html>’, you provide the javascript. Not much is needed. The whole of it, first data then code, is as follows:

<script>
// data 
var stem = 'https://www.routemaster.app/?track=' +
           'https://www.masterlyinactivity.com/routemaster/routes/' ;
var gpsim = { imgurl:'https://www.masterlyinactivity.com/includes/NorthItaly.gif' , 
              srcset:'https://www.masterlyinactivity.com/includes/NorthItaly@h.gif 2x' , 
              w:174 , h:100 ,
              linkurl:stem+'NorthItaly.rte' ,
              map: [ { title:'Lake Como' , 
                       linkurl:stem+'altarezia16/larioindex.rte' ,
                       coords:[45,60,55,40,25] } , 
                     { title:'L. Garda (north)' , 
                       linkurl:stem+'garda/NGarda.rte' ,
                       coords:[145,65,12] } , 
                     { title:'L. Garda (east)' , 
                       linkurl:stem+'garda/EGarda.rte' ,
                       coords:[135,85,12] } , 
                     { title:'Alta Rezia' , 
                       linkurl:stem+'altarezia16/index.rte' ,
                       coords:[80,30 , 120,40 , 110,20 , 140,35 , 120,0 , 85,5 ] } , 
                     { title:'L. Maggiore' , 
                       linkurl:stem+'maggiore/LagoMaggiore.rte' ,
                       coords:[15,55,20] } , 
                     { title:'Lake Iseo' , 
                       linkurl:stem+'iseo/LagodIseo.rte' ,
                       coords:[80,50,110,90] } ] } ;
var regionstyle = { borderColor:"#aa0000" , borderWidth:1 , 
                    outfillColor:"grey" ,   outfillOpacity:0.4 , 
                    touchBorderOpacity:0.4 } ; 
var mapstyle    = { class:'bordered' , x:3 , y:3 } ;

// code
var img = genmappedimage(gpsim,mapstyle,regionstyle) ;
var imgel = document.getElementById('imgdiv') ;
imgel.parentNode.replaceChild(img,imgel) ; 
addtitletomappedimage(gpsim,document.getElementById('imghead')) ; 
</script>

It is the data component which most needs explanation. Ignore the variable ‘stem’, which is simply used to abbreviate my URLs, which are longer than most people ever need worry about.

The main piece of data is the object – assigned here to the variable gpsim – which defines the image containing mapped regions. Its obligatory fields are:

If there are no additional fields in the map object, then there will be no mapped regions inside the image.

The optional fields of a map object are:

Some further fields will be added by imagemap during its processing.

The region fields are:

The coordinates are an array of 3, 4, 5 or 2n numbers with n≥3.

The map contains examples of all 4 shapes. The polygon (Alta Rezia) is exaggeratedly ugly to illustrate the fact that regions need not be convex.

Two structures are used to pass style information.

The image style contains style information about the image. Its fields are:

North Italy
The region style determines the appearance of a region hovered over. Its fields are: borderColor, borderWidth, outfillColor, outfillOpacity and touchBorderOpacity. These specify the width and colour of the border and the colour and opacity of the outfill, i.e. of the fill applied to the image outside the region hovered over. Setting the width/opacity to 0 disables the border/outfill. (Don’t attempt east Atlantic spelling.)

touchBorderOpacity is provided to make map regions visible on touch-screen devices where hovering cannot be detected, and where the normal borders and outfill therefore cannot be drawn reactively. On such devices it is a good idea to draw an unobtrusive permanent border round all the regions. This can be requested by supplying a non-zero touchBorderOpacity, which will be applied in conjunction with the standard borderColor, but only on touch-screen devices. The effect is as shown on the right.

Setting the the map object field debug to ‘touch’ tells imagemap to emulate touch-screen processing for the map in question; this is how the diagram on the right is obtained. You will find that clicking in the different regions acts as for an HTML image map, even though there’s no reactive response.

The image is generated in 3 steps. The first is written as

img = genmappedimage(gpsim,mapstyle,regionstyle) ;

which returns a div containing a mapped image according to the supplied parameters.

The second step is to put the div into the document in place of its placeholder. The placeholder is identified by its ‘id’ – in this case ‘imgdiv’ – so we find it by a call to getElementById(), and then replace the placeholder as a child of its parent by the newly created div.

var imgel = document.getElementById('imgdiv') ;
imgel.parentNode.replaceChild(img,imgel) ; 

It would be slicker to do this in a single line:

document.getElementById('imgdiv').replaceWith(img) ;

but the replaceWith() function was not added to browsers until 2016, so you will be cutting off users with very old computers.

The final step is optional, and is needed only if you want the image title to be changed to reflect the region hovered over. You call

addtitletomappedimage(gpsim,document.getElementById('imghead')) ;

The first argument is the map object holding information relative to the image, and the second is the element whose inner HTML needs to be changed, found from its ‘id’. If you do not supply a title to update, the titles of the mapped regions will be displayed as tooltips.

imagemap was written for our home page, which nowadays is pure javascript. Take a look at the source code to see the full process. The component of interest is the function addgps whose current source code is this:

function addgps(div,i,marg)
{ var i , map , p ;   
  i = gpsind(i) ; 
  if(!addgps.ims) addgps.ims = new Array(gpsims.length) ; 
  if(!addgps.ims[i])
  { p = document.createElement('p') ;
    p.innerHTML = gpsims[i].title ;
    addtitletomappedimage(gpsims[i],p) 
    p.setAttribute('style','margin-top:'+marg+'px') ; 
    addgps.ims[i] = genmappedimage(gpsims[i],{x:3,y:3},regionstyle) ;
  }   
  div.appendChild(gpsims[i].titlediv) ; 
  div.appendChild(addgps.ims[i]) ; 
}

div is a div supplied to addgps to which the new image will be added. i is the name of the image, which is converted to an index in an array by the call to gpsind. There follows some conditional code: addgps may be called repeatedly as the page is reconstructed following resizing, but we don’t want to repeat work unnecessarily, especially if it may give rise to HTTP requests, so the mapped images are stored in a static array once created. marg is a variable passed to parametrise the top margin.

To add an image to the div, we start by creating an element containing the title. We use the ‘title’ field of the map object to store the title we will use, knowing that it will be subsequently overwritten by the title as actually stored in the document (which will be equivalent as HTML, though not necessarily as a character string). We place this element in the map object by calling addtitletomappedimage, which puts it in the titlediv field. Then we create the mapped image itself by calling genmappedimage. Notice that the image inherits its style from a global css declaration, so the only map style fields we need to supply are x and y.

To add the title and image to the div, we then successively append the corresponding elements, the first of which has been stored in the titlediv field of the map object, and the second of which has been kept in a static array.

As mentioned earlier, imagemap stores values in the map object passed to it as well as reading values from it. It overwrites the following fields:

Email me at colin·champion&routemaster·app, substituting full stops for the dots and an ampersat for the ampersand.

imagemap is provided under an MIT licence permitting free use and modification. It does the things I wanted it to do for our home page; other features might be both useful and easy to add. Feel free. Let me know how you get on.

HTML header : HTML body : additional javascript : map structure : region structure : style structures : code for image generation : creating a mapped image in javascript : hints : map object fields overwritten by imagemap : contact me : usage rights

• genmappedimage     • mousefactory     • function     • leavefactory     • clickfactory     • addtitletomappedimage     • outpoint     • findregion     • pointinpoly     • isleft     • getbordercoords     • getp     • drawshape     • getpt     • imagetwitch     • setlink

/* ------- https://www.masterlyinactivity.com/software/imagemap.html ------- */

/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
*/

function genmappedimage(image,imagestyle,regionstyle)
{ function mousefactory(img) 
  { return function(e) 
    { imagetwitch(e,img,regionstyle.borderColor,regionstyle.borderWidth,
                        regionstyle.outfillColor,regionstyle.outfillOpacity) ; 
    } ; 
  } 
  function leavefactory(img) 
  { return function(e) 
    { imagetwitch(null,img,regionstyle.borderColor,regionstyle.borderWidth,
                        regionstyle.outfillColor,regionstyle.outfillOpacity) ; 
    } ; 
  } 
  function clickfactory(img) 
  { return function(e) 
    { var i = findregion(e,image) ; 
      if(i!=null) image.canlink.setAttribute('href',image.map[i].linkurl) ; 
    } ; 
  } 

  var c,dpr,i,k,p,pdash,q,n,m=image.map,mini,minx,mi,wise,co ;

  // create the image
  image.div = document.createElement('div') ;
  image.div.style.position = 'relative' ; 

  image.img = document.createElement('img') ;
  if(imagestyle&&imagestyle.class) 
    image.img.setAttribute('class',imagestyle.class) ; 
  if(imagestyle&&imagestyle.style) 
    image.img.setAttribute('style',imagestyle.style) ; 
  image.img.setAttribute('width',image.w) ; 
  image.img.setAttribute('height',image.h) ; 
  if(image.srcset) image.img.setAttribute('srcset',image.srcset) ; 
  image.img.setAttribute('src',image.imgurl) ; 

  image.link = document.createElement('a') ;
  image.link.setAttribute('href',image.linkurl?image.linkurl:'#') ; 
  if(!image.linkurl) image.link.style.cursor = 'default' ;

  image.link.appendChild(image.img) ; 
  image.div.appendChild(image.link) ; 
  if(!image.map) return image.div ; 

  // remaining code is for handling image map if provided

  // put the coords into canonical form
  for(mi=0;mi<m.length;mi++) 
  { p = m[mi].coords ; 
    if(p.length&1) 
    { if(p.length==5&&p[0]>p[2]) 
        m[mi].coords = [ p[2],p[3] , p[0],p[1] , p[4] ] ;
      continue ; 
    }
    n = p.length / 2 ; 
    if(n==2) 
    { m[mi].coords = [ Math.min(p[0],p[2]) , Math.min(p[1],p[3]) , 
                       Math.max(p[0],p[2]) , Math.max(p[1],p[3]) ] ;
      continue ; 
    }
    // remove end pt if a dupe of start
    while(n>=2&&p[2*(n-2)]==p[2*(n-1)]&&p[2*(n-2)+1]==p[2*(n-1)+1]) n -= 1 ; 

    // reorder so that leftmost point comes first
    for(i=0;i<n;i++) if(i==0||p[2*i]<minx) { mini = i ; minx = p[2*i] ; }
    pdash = new Array(2*n) ; 
    for(k=0,i=mini;k<n;i++,k++)
    { if(i==n) i = 0 ; pdash[2*k] = p[2*i] ; pdash[2*k+1] = p[2*i+1] ; }
    p = pdash ; 

    // reorder the points anticlockwise
    for(q=(p[0]-p[2*(n-1)])*(p[1]+p[2*(n-1)+1]),i=0;i<n-1;i++) 
      q += (p[2*(i+1)]-p[2*i]) * (p[2*(i+1)+1]+p[2*i+1]) ;
    if(q<0) for(i=1;2*i<n;i++) for(k=0;k<2;k++)
    { q = p[2*i+k] ; p[2*i+k] = p[2*(n-i)+k] ; p[2*(n-i)+k] = q ; }
    m[mi].coords = p ; 
  }

  // create an empty canvas with suitable attributes
  c = document.createElement('canvas') ; 
  image.offs = { x:0 , y:0 } ;
  if(imagestyle&&imagestyle.x) image.offs.x = imagestyle.x ; 
  if(imagestyle&&imagestyle.y) image.offs.y = imagestyle.y ; 
  c.setAttribute('style','width:'+image.w+'px;height:'+image.h+'px;'+
                         'position:absolute;left:'+image.offs.x+'px;'+
                         'top:'+image.offs.y+'px') ;

  if(image.debug!='touch'&&!window.matchMedia("(hover: none)").matches)
  { c.onmouseover = mousefactory(image) ; 
    c.onmousemove = mousefactory(image) ; 
    c.onmouseout = leavefactory(image) ; 
  }
  c.onclick = clickfactory(image) ; 

  dpr = window.devicePixelRatio ;
  c.width = Math.floor(dpr*image.w) ; 
  c.height = Math.floor(dpr*image.h) ; 
  image.ovl = ctx = c.getContext('2d') ;
  ctx.scale(dpr,dpr) ; 

  if(image.debug=='touch'||window.matchMedia("(hover: none)").matches)
    if(regionstyle.touchBorderOpacity) for(i=0;i<m.length;i++)
  { p = getp(m[i]) ; 
    if((co=getbordercoords(m[i],p))) 
      drawshape(ctx,p,co,regionstyle.borderColor,regionstyle.borderWidth,
                regionstyle.touchBorderOpacity) ;
  }

  image.div.appendChild(image.link) ; 
  image.canlink = document.createElement('a') ;
  image.canlink.setAttribute('href',image.linkurl?image.linkurl:'#') ; 
  image.canlink.appendChild(c) ; 
  image.div.appendChild(image.canlink) ; 
  if(!image.linkurl) image.canlink.style.cursor = 'default' ;

  return image.div ; 
}
/* -------------------------------------------------------------------------- */

function addtitletomappedimage(image,titlediv) 
{ image.titlediv = titlediv ; image.title = titlediv.innerHTML ; }

/* -------------------------------------------------------------------------- */

// outpoint computes the offset from p1 (the inner vertex of an anticlockwise  
// polygon) to the corresponding outer vertex on its border, assuming a border
// width of 1, when p0 and p2 are the preceding and following vertices
// following https://stackoverflow.com/questions/36722826/calculate-...
// the-outer-vertices-for-a-border-of-uniform-thickness-x-drawn-around-a

function outpoint(p0,p1,p2)
{ var cotalpha , sintheta , costheta , d0 , d2 , sinalpha , cvx;
  // shift coordinates to put p1 at the origin
  var x0=p0.x-p1.x,y0=p0.y-p1.y,x2=p2.x-p1.x,y2=p2.y-p1.y ;
  d0 = Math.sqrt(x0*x0+y0*y0) ; 
  d2 = Math.sqrt(x2*x2+y2*y2) ; 
  // 𝜽 is the angle subtended by the line p1–p2 to the x-axis;
  // 𝜶 is half the additional angle needed to reach the line p1–p0
  sinalpha = Math.sqrt((d2*x0-d0*x2)*(d2*x0-d0*x2)+(d2*y0-d0*y2)*(d2*y0-d0*y2)) /
                    (2*d0*d2) ;
  if(sinalpha<1e-10) return null ;

  cotalpha = Math.sqrt(1-sinalpha*sinalpha) / sinalpha ;
  // this gives us the magnitude but we also need the sign
  if(x0*y2>x2*y0) cotalpha = -cotalpha ;

  costheta = x2 / d2 ; 
  sintheta = y2 / d2 ; 
  return { x:costheta*cotalpha-sintheta , y:costheta+sintheta*cotalpha } ; 
}
/* -------------------------------------------------------------------------- */

function findregion(e,image)
{ var i , m=image.map , pt ;
  if(!e||!m) return null ; 
  pt = [ e.pageX-image.div.offsetLeft-image.offs.x,
         e.pageY-image.div.offsetTop -image.offs.y ] ;
  if(image.debug==1||image.debug=='1') console.log('('+pt[0]+','+pt[1]+')') ; 

  for(i=0;i<m.length&&!pointinpoly(pt,m[i].coords);i++) ;
  if(i<m.length) return i ; else return null ; 
}
/* -------------------------------------------------------------------------- */

// based on https://gist.github.com/vlasky/d0d1d97af30af3191fc214beaf379acc
// by Vlad Lasky

function pointinpoly(point,vtx) 
{ var x=point[0],y=point[1],wn,i,j,n=vtx.length/2 ;
  if(vtx.length==3) // circle
    return (x-vtx[0])*(x-vtx[0]) + (y-vtx[1])*(y-vtx[1]) <= vtx[2]*vtx[2] ;
  if(vtx.length==5) // ellipse
     return Math.sqrt((x-vtx[0])*(x-vtx[0])+(y-vtx[1])*(y-vtx[1])) +
            Math.sqrt((x-vtx[2])*(x-vtx[2])+(y-vtx[3])*(y-vtx[3])) <= 2*vtx[4] ;
  if(n==2) return vtx[0]<=x && x<=vtx[2] && vtx[1]<=y && y<=vtx[3] ; // rectangl

  function isleft(xj,yj,xi,yi,x,y) { return (xi-xj)*(y-yj) - (x-xj)*(yi-yj) ; }

  for(wn=i=0;i<n;i++)
  { j = (i?i-1:n-1) ;
    if(vtx[2*j+1]<=y) 
    { if(vtx[2*i+1]>y&&isleft(vtx[2*j],vtx[2*j+1],vtx[2*i],vtx[2*i+1],x,y)>0) 
        wn += 1 ; 
    }
    else 
    { if(vtx[2*i+1]<=y&&isleft(vtx[2*j],vtx[2*j+1],vtx[2*i],vtx[2*i+1],x,y)<0) 
      wn -= 1 ; 
    }
  }
  return wn != 0 ; 
}
/* -------------------------------------------------------------------------- */

function getbordercoords(m,p) 
{ if(m.bordercoords) return m.bordercoords ;
  var c = new Array(n) , i , n=p.length , c , p0 , p1 , p2 , s; 

  if(n==5)
  { c[0] = (p[0]+p[2]) / 2 ; 
    c[1] = (p[1]+p[3]) / 2 ;                      // coords of centre
    s = (p[1]-c[1])*(p[1]-c[1]) + (p[0]-c[0])*(p[0]-c[0]) ; 
    c[3] = Math.sqrt(s) ;                         // dist from centre to foci
    if(c[3]==0) c[2] = 0 ; 
    else c[2] = Math.atan2(p[3]-p[1],p[2]-p[0]) ; // orientation of major axis
    if(p[4]*p[4]-s<0) c = null ; 
    else c[4] = Math.sqrt(p[4]*p[4]-s) ;          // minor axis (major is p[4])
  }
  else for(i=0;i<n;i+=2) 
  { if(i==0) p0 = { x:p[n-2] , y:p[n-1] } ; else p0 = { x:p[i-2] , y:p[i-1] } ; 
    if(i==n-2) p2 = { x:p[0] , y:p[1] } ; else p2 = { x:p[i+2] , y:p[i+3] } ; 
    if(!(p1=outpoint(p0,{x:p[i],y:p[i+1]},p2))) { c = null ; break ; }
    else { c[i] = p1.x ; c[i+1] = p1.y ; }
  }

  if(c) return m.bordercoords = c ; 
  else if(p.length==5) alert('impossible ellipse supplied for '+m.title) ; 
  else alert('degenerate polygon supplied for '+m.title) ;
  return null ; 
}
function getp(m)
{ var p = m.coords ; 
  if(p.length==3) return [ p[0],p[1] , p[0],p[1] , p[2] ] ;
  else if(p.length==4) 
    return [ p[0],p[1] , p[0],p[3] , p[2],p[3] , p[2],p[1] ] ; 
  else return p ;
}
/* -------------------------------------------------------------------------- */

function drawshape(ctx,p,c,colour,h,opacity,imagew,imageh)
{ var i , n = p.length/2 , qx , pt ;
  ctx.fillStyle = colour ; 
  if(opacity) ctx.globalAlpha = opacity ; else ctx.globalAlpha = 1 ;

  ctx.beginPath() ; 
  if(!h)               // draw surrounding rectangle
  { pt = getpt(p,c) ; 
    qx = Math.min(0,pt[0]) ;
    ctx.moveTo(qx,pt[1]) ;
    ctx.lineTo(qx,0) ; 
    ctx.lineTo(imagew,0) ;      
    ctx.lineTo(imagew,imageh) ;
    ctx.lineTo(qx,imageh) ;
    ctx.lineTo(qx,pt[1]) ; 
  }
  else if(p.length==5) // draw outer ellipse
    ctx.ellipse(c[0],c[1],p[4]+h,c[4]+h,c[2],0,2*Math.PI) ; 
  else                 // draw outer polygon
  { ctx.moveTo(p[0]+h*c[0],p[1]+h*c[1]) ; 
    for(i=n-1;i>=0;i--) ctx.lineTo(p[2*i]+h*c[2*i],p[2*i+1]+h*c[2*i+1]) ;
  }

  if(p.length==5)      // draw inner ellipse
    ctx.ellipse(c[0],c[1],p[4],c[4],c[2],0,2*Math.PI,1) ; 
  else                 // draw inner polygon
  { for(i=0;i<n;i++) ctx.lineTo(p[2*i],p[2*i+1]) ; 
    ctx.lineTo(p[0],p[1]) ; // needed?
  }
  ctx.fill() ; 
}
/* -------------------------------------------------------------------------- */

function getpt(p,c)
{ // if the region is an ellipse, we compute the coordinates of the
  // point from which we start drawing, which is a point on the minor axis
  var pt = [0,0] ;
  if(p.length==5)
  { if(c[3]==0) { pt[0] = 0 ; pt[1] = -c[4] ; } 
    else
    { pt[0] = - c[4] * (p[3]-p[1]) / (2*c[3]) ; 
      pt[1] = c[4] * (p[2]-p[0]) / (2*c[3]) ; 
    }
    if(pt[0]>0) { pt[0] = -pt[0] ; pt[1] = -pt[1] ; }
    pt[0] += c[0] ; 
    pt[1] += c[1] ; 
  }
  else { pt[0] = p[0] ; pt[1] = p[1] ; }
  return pt ; 
}
/* -------------------------------------------------------------------------- */

function imagetwitch(e,image,bordercolour,h,outfillcolour,outfillopacity)
{ var i , m=image.map , p , c , pt , ctx = image.ovl ; 
  if(image.region==undefined) image.region = null ; 

// --- vvv nested function
function setlink(title,linkurl)
{ if(image.titlediv) 
  { while(image.titlediv.lastChild)
      image.titlediv.removeChild(image.titlediv.lastChild) ; 
    image.titlediv.innerHTML = title ; 
  }
  image.link.setAttribute('href',linkurl?linkurl:'#') ; 
  image.canlink.setAttribute('href',linkurl?linkurl:'#') ; 
  if(linkurl) 
  { image.link.style.cursor = image.canlink.style.cursor = 'pointer' ;
    if(!image.titlediv) image.link.title = image.canlink.title = title ;
  }
  else 
  { image.link.style.cursor = image.canlink.style.cursor = 'default' ;
    image.link.removeAttribute(title) ; 
    image.canlink.removeAttribute(title) ; 
  }
}
// --- ^^^ nested function

  i = findregion(e,image) ; 
  if(i==image.region) return ; else image.region = i ; 
  if(i==null) 
  { ctx.clearRect(0,0,image.w,image.h) ; 
    setlink(image.title,image.linkurl) ; 
    return ; 
  }

  m = m[image.region] ;
  ctx.clearRect(0,0,image.w,image.h) ; 

  setlink(m.title,m.linkurl) ; 
  if((!bordercolour||!h)&&(!outfillcolour||outfillopacity==0)) return ; 

  p = getp(m) ; 
  if(!(c=getbordercoords(m,p))) return ; 

  if(outfillcolour&&outfillopacity!=0)
  { if(!outfillopacity) outfillopacity = 1 ; 
    drawshape(ctx,p,c,outfillcolour,0,outfillopacity,image.w,image.h) ;
  }
  if(bordercolour&&h) drawshape(ctx,p,c,bordercolour,h,0) ;
}