// ******************************
// *** class Map              ***
// ******************************
function Map(config, winCtrl) {
  var self=this, map=this;


  // *** various data ***
  var evalXPath=self.evalXPath=HelperFunctions.getEvalXPathFunction({tmb:'http://reiseland-brandenburg.de/XML', map:'http://reiseland-brandenburg.de/XML/map', xhtml:'http://www.w3.org/1999/xhtml'});
  self.winCtrl=winCtrl;
  self.htmlElems={
    tiles:document.getElementById('jsMap-tiles')
    , icons:document.getElementById('jsMap-icons')
  };
  self.state=new State();
  self.scales=new Scales(self);
  self.tiles=new Tiles(self);
  self.tours=null; (function() {
    if(!config.minimap) self.tours=new Tours(self);
  })();
  self.icons=new Icons(self);
  self.bboxMarker=new BBoxMarker(self);
  self.geoPosMarker=new GeoPosMarker(self);
  self.infoBox=new InfoBox(self);
  self.tooltip=new Tooltip(self);
  self.pois=new POIs(self);

  var intervalPeriod=66.7;


  // *** positioning, resizing, switching scales ***

  self.size=null;
  self.setSize=function(w, h) {
    var styleTiles=self.htmlElems.tiles.style
        , styleIcons=self.htmlElems.icons.style
    ;
    styleIcons.width=styleTiles.width=(w)+'px';
    styleIcons.height=styleTiles.height=(h)+'px';
    self.size=[w, h];
    self.setPosition();
    self.positionElements();
  }

  self.pos=[0, 0];

  var intendedGeoPos=new (function(map) {
    var self=this
        , pos=null
    ;
    self.set=function(p) {
      if(p) pos=[p[0], p[1]];
      else pos=null;
      if( !map.state.contains('dontNotify') ) winCtrl.onSetIntendedGeoPosition(pos);
    };
    self.get=function() { return pos ? [pos[0], pos[1]] : null ; };
  })(self);
  self.setIntendedGeoPosition=function(p) { return intendedGeoPos.set(p); };
  
  self.posHTMLToScale=function(pos) { return [pos[0] - (self.pos[0] - self.size[0]/-2), pos[1] - (self.pos[1] - self.size[1]/-2)]; }
  self.posScaleToHTML=function(pos) { return [pos[0] - (self.size[0]/-2 - self.pos[0]), pos[1] - (self.size[1]/-2 - self.pos[1])]; }

  self.positionElements=function() {
    positionTilesAndIcons();
    self.infoBox.position();
    self.tooltip.position();
    self.bboxMarker.position();
    self.geoPosMarker.position();
    if(self.tours) self.tours.position();
  }
  function positionTilesAndIcons() {
    self.tiles.check();
    var dx=Math.round(self.size[0]/-2)-self.pos[0]
        , dy=Math.round(self.size[1]/-2)-self.pos[1]
        , gridSize=self.tiles.getGridSize()
    ;
    for(var pos in self.tiles.tiles) {
      var tile=self.tiles.tiles[pos]
          , styleGfx=tile.gfx.style
          , styleIcons=tile.icons.style
      ;
      pos=self.tiles.pos2coords(pos);
      styleIcons.left=styleGfx.left=(pos[0]-dx)+'px';
      styleIcons.top=styleGfx.top=(pos[1]-dy)+'px';
      styleIcons.width=styleGfx.width=(gridSize)+'px';
      styleIcons.height=styleGfx.height=(gridSize)+'px';
    }
  }

  self.setPosition=function(pos) {
    if(pos==undefined) pos=[self.pos[0], self.pos[1]];
    var scale=self.scales.get()
        , minX=Math.round(self.size[0]/2)-scale.width
        , maxX=scale.width-Math.round(self.size[0]/2)
        , minY=Math.round(self.size[1]/2)-scale.height
        , maxY=scale.height-Math.round(self.size[1]/2)
    ;
    if(minX>maxX) minX=maxX=0;
    if(minY>maxY) minY=maxY=0;
    self.pos[0]= Math.max(minX, Math.min(pos[0], maxX)) ;
    self.pos[1]= Math.max(minY, Math.min(pos[1], maxY)) ;
    if( !self.state.contains('dontNotify') ) {
      winCtrl.onMapSetPosition( self.scales.posScaleToGeo(self.pos) );
    }
  }
  self.setGeoPosition=function(pos) {
    var bbox;
    bbox=self.setPosition( self.scales.posGeoToScale(pos) );
    self.positionElements();
    return bbox;
  }

  self.getBBox=function() {
    var sizeGeo=self.scales.scaleScaleToGeo([self.size[0]/2, self.size[1]/2])
        , posGeo=self.scales.posScaleToGeo([-self.pos[0], -self.pos[1]])
    ;
    return [ [posGeo[0]-sizeGeo[0], posGeo[1]-sizeGeo[1]] , [posGeo[0]-(-sizeGeo[0]), posGeo[1]-(-sizeGeo[1])] ];
  }

  self.switchScale=function(sclNr, sclNrRel) {
    var f;
    if(sclNrRel) sclNr=Math.max(0, Math.min(self.scales.current-(-sclNrRel), self.scales.scales.length-1));
    if(sclNr==self.scales.current) return sclNr;
    if(self.size) {
      var cur=self.scales.get()
          , next=self.scales.get(sclNr)
      ;
      f=[next.width/cur.width, next.height/cur.height];
    }
    if(!intendedGeoPos.get()) intendedGeoPos.set( self.scales.posScaleToGeo([-self.pos[0], -self.pos[1]]) );
    self.scales.switchTo(sclNr);
    if(!self.size) return sclNr;
    var pos= intendedGeoPos.get() ? self.scales.posGeoToScale(intendedGeoPos.get()) : [-f[0]*self.pos[0], -f[1]*self.pos[1]] ;
    self.setPosition([-pos[0], -pos[1]]);
    self.tiles.removeAll();
    self.positionElements();
    return sclNr;
  }

  var move={
    velocity:[0, 0]
    , acceleration:1
    , intervalFunction:function() {
        this.acceleration=Math.min(16, this.acceleration*1.05);
        map.setPosition([
            map.pos[0] - this.acceleration*this.velocity[0]
          , map.pos[1] - this.acceleration*this.velocity[1]
        ]);
        map.positionElements();
      }
  }
  self.addMove=function(v) {
    intendedGeoPos.set();
    move.velocity[0]-=v[0];
    move.velocity[1]-=v[1];
    if(move.velocity[0] || move.velocity[1]) {
      if(!move.interval) {
        self.infoBox.hide();
        move.interval=window.setInterval(function() { return move.intervalFunction(); }, intervalPeriod);
      }
    } else if(move.interval) {
      window.clearInterval(move.interval);
      delete move.interval;
      move.acceleration=1;
      self.infoBox.show();
    }
  }

  var moveTo={
    pos:null, last:null, interval:null, callback:null
    , intervalFunction:function() {
        var   dx=(-map.pos[0]-this.pos[0])/-2
            , dy=(-map.pos[1]-this.pos[1])/-2
        ;
        map.setPosition([map.pos[0]-dx, map.pos[1]-dy]);
        map.positionElements();
        if( Math.abs(map.pos[0]-this.last[0])<1 && Math.abs(map.pos[1]-this.last[1])<1 ) {
          window.clearInterval(this.interval);
          this.interval=null;
          if(this.callback) {
            this.callback();
            this.callback=null;
          }
          map.infoBox.show();
        } else {
          this.last[0]=map.pos[0];
          this.last[1]=map.pos[1];
        }
      }
  };
  self.moveToScalePosition=function(pos, callback) {
    moveTo.last=[self.pos[0], self.pos[1]];
    moveTo.pos=[pos[0], pos[1]];
    moveTo.callback=callback;
    if(!moveTo.interval) {
      self.infoBox.hide();
      moveTo.interval=window.setInterval(function() { return moveTo.intervalFunction(); }, intervalPeriod);
    }
  }
  self.moveToGeoPosition=function(pos, callback) {
    intendedGeoPos.set();
    self.moveToScalePosition( self.scales.posGeoToScale(pos) , callback );
  }
  self.moveToBBox=function(min, max) {
    if(!min||!max) {
      var bbox=self.scales.getMaxGeoBBox(.95);
      min=bbox[0];
      max=bbox[1];
    }
    self.switchScale( self.scales.findFittest([Math.abs(max[0]-min[0]), Math.abs(max[1]-min[1])]) );
    var p=[-(-min[0]-max[0])>>1, -(-min[1]-max[1])>>1];
    self.moveToGeoPosition(p);
    self.setIntendedGeoPosition(p);
  }

  self.markGeoPos=function(pos) { self.geoPosMarker.set(pos); }
  
  
  // *** mouse handling ***

  var lastOnMouseMove=null, lastMousePos=null;

  function getHTMLMousePos(e) { return [e.clientX, e.clientY]; }

  self.onMapMouseDown=function(e) {
    if(!e) e=window.event;
    lastMousePos=getHTMLMousePos(e);
    self.state.set('mapMouseDown');
    return HelperFunctions.stopPropagation(e);
  }
  self.htmlElems.icons.onmousedown=self.onMapMouseDown;

  self.onMouseUp=function(e) {
    if(!e) e=window.event;
    if( self.state.contains('mapMouseDown') ) {
      self.state.clear('mapMouseDown');
      if( self.state.contains('mouseMoved') ) {
        self.state.clear('mouseMoved');
        self.infoBox.show();
      } else {
        var posScl=self.posHTMLToScale( getHTMLMousePos(e) )
            , posGeo=self.scales.posScaleToGeo(posScl)
        ;
        intendedGeoPos.set(posGeo);
        if( !config.minimap ) {
          self.moveToScalePosition(posScl);
        } else if( !self.state.contains('dontNotify') ) {
          winCtrl.onMapMoveToPosition(posGeo);
        }
      }
    }
    if(documentOnmouseup) return documentOnmouseup.call(document, e);
  }
  var documentOnmouseup=document.onmouseup;
  document.onmouseup=self.onMouseUp;

  self.onMouseMove=function(e) {
    if(!e) e=window.event;
    if( self.state.contains('mapMouseDown') && !config.minimap ) {
      if( !self.state.contains('mouseMoved') ) {
        self.state.set('mouseMoved');
        self.infoBox.hide();
      }
      var t=new Date().getTime();
      if(t-lastOnMouseMove<intervalPeriod) return;
      lastOnMouseMove=t;
      var mousePos=getHTMLMousePos(e);
      intendedGeoPos.set();
      self.setPosition([
          self.pos[0]-(lastMousePos[0]-mousePos[0])
        , self.pos[1]-(lastMousePos[1]-mousePos[1])
      ]);
      lastMousePos=mousePos;
      self.positionElements();
    }
    if(documentOnmousemove) return documentOnmousemove.call(document, e);
  }
  var documentOnmousemove=document.onmousemove;
  document.onmousemove=self.onMouseMove;


  // *** miscellaneous ***
  self.onVCardClick=function(uri) { winCtrl.onVCardClick(uri); }
  self.getVCardURI=function(elem) {
    var rubOpt=elem.rubXML ? elem.rubXML.getAttribute('opt') : null
        , id=null, uri=null, urlIC=null
    ;
    if( urlIC=evalXPath(elem.objXML, 'tmb:url[@user="ic"]/text()')[0] ) {
      uri=urlIC.nodeValue;
    } else if( winCtrl.config.user=='ic' && (id=elem.objXML.getAttribute('id')) && (id=id.match(/^\d+$/)) ) {
      (uri=winCtrl.config.parentURI) && (uri=uri.match(/^((https?:\/\/)[^\/?#]+)/)) && (uri=uri[1])
        || (uri='http://'+(winCtrl.config.layout.pts?'www.potsdamtourismus.de':'www.reiseland-brandenburg.de'))
      ;
      uri+='/shadowbox/detail/bbid/' + id[0] + '.html';
    } else if( rubOpt && -1<rubOpt.indexOf('JSMAP_TRAINSTATION') ) {
      uri=winCtrl.vcardBaseURI+'trainstation.php?id='+elem.objXML.getAttribute('id')+map.winCtrl.getExtraQuery();
    } else if( elem.objXML && (id=elem.objXML.getAttribute('id')) && id.match(/^\d+$/) ) {
      uri=winCtrl.vcardBaseURI+'visitenkarte.php?id='+elem.objXML.getAttribute('id')+map.winCtrl.getExtraQuery();
      var docElem=elem.objXML.ownerDocument.documentElement;
      if( docElem.getAttribute('start') && docElem.getAttribute('end') ) {
        uri+='&start='+docElem.getAttribute('start')+'&end='+docElem.getAttribute('end');
      }
    }
    return uri;
  }

  self.hasFilledTour=function(objXML) {
    var opt=objXML.getAttribute('opt');
    return opt && opt.match(/(^|,)TOUR:FILL(,|$)/) ;
  }

}






//******************************
//*** class GeoPosMarker       ***
//******************************
function GeoPosMarker(map) {
  var self=this
      , pos=null
      , elem=null;
  ;
  self.set=function(_pos) {
    self.rem();
    if(!_pos) return;
    pos=map.scales.posGeoToRef(_pos);
    elem=document.createElement('div');
    elem.id='geoPosMarker';
    map.htmlElems.tiles.appendChild(elem);
    self.position();
  }
  self.rem=function() {
    if(elem) {
      elem.parentNode.removeChild(elem);
      pos=null;
    }
  }
  self.position=function() {
    if(!elem) return;
    var _pos=map.posScaleToHTML( map.scales.posRefToScale(pos) )
        , style=elem.style
    ;
    style.left=( Math.round( _pos[0] ) )+'px';
    style.top=( Math.round( _pos[1] ) )+'px';
  }
}






//******************************
//*** class BBoxMarker       ***
//******************************
function BBoxMarker(map) {
  var self=this
      , elem=null, bbox=null
  ;
  self.set=function(givenBBox) {
    if(!givenBBox) {
      if(elem) {
        elem.parentNode.removeChild(elem);
        elem=null;
      }
      return;
    }
    if(!elem) {
      elem=document.createElement('div');
      elem.id='bboxMarker';
      elem.appendChild( document.createElement('div') );
      map.htmlElems.tiles.appendChild(elem);
    }
    bbox=[ map.scales.posGeoToRef([givenBBox[0][0], givenBBox[1][1]]) , map.scales.posGeoToRef([givenBBox[1][0], givenBBox[0][1]]) ];
    self.position();
  }
  self.position=function() {
    if(!elem) return;
    var min=map.posScaleToHTML( map.scales.posRefToScale(bbox[0]) )
        , max=map.posScaleToHTML( map.scales.posRefToScale(bbox[1]) )
        , style=elem.style
    ;
    style.left=( Math.round( min[0] ) )+'px';
    style.top=( Math.round( min[1] ) )+'px';
    style.width=( Math.max(2, Math.round(max[0]-min[0])) )+'px';
    style.height=( Math.max(2, Math.round(max[1]-min[1])) )+'px';
  }
}






//******************************
//*** class Tooltip          ***
//******************************
function Tooltip(map) {
  var self=this
      , elem=null
      , pos=null
      , htmlPos=null
      , poiGroup=null
      , regexp={}
      , timeout=null
  ;
  var
    tourTexts={'false':map.winCtrl.config.lang['infobox.tour.show'], 'true':map.winCtrl.config.lang['infobox.tour.hide']}
    , areaTexts={'false':map.winCtrl.config.lang['infobox.area.show'], 'true':map.winCtrl.config.lang['infobox.area.hide']}
  ;
  var show=self.show=function(givenPos, givenPOIGroup) {
    if( !map.winCtrl.config.layout.tooltip || map.state.contains('mapMouseDown') ) return;
    clearTimeout();
    hideTimeout();
    pos=givenPos;
    poiGroup=givenPOIGroup;
    elem=document.createElement('div');
    elem.id='tooltip';
    for(var i=0; i<poiGroup.length; i++) {
      var line=document.createElement('a')
        , objXML=poiGroup[i].objXML
        , catXML=map.evalXPath(objXML, '//tmb:category[@id=\''+objXML.getAttribute('cat')+'\']')[0]
        , rubXML=map.evalXPath(objXML, '//tmb:rubric[@id=\''+objXML.getAttribute('rub')+'\']')[0]
        , isInactive=map.icons.isInactive(objXML, line.catXML)
        , nr=map.evalXPath(objXML, '@*[local-name()="nr"]')
      ;
      var name=objXML.getAttribute('name');
      if( name.length>40 ) {
        name=name.replace(/\s+/, ' ').replace(/^\s*(.*\S)\s*$/, '$1').split(' ');
        for(var len=0, j=0; j<name.length; j++) {
          if( (len=len+1+name[j].length) <= 40 ) continue;
          while(name.length>j) name.pop();
          break;
        }
        name=name.join(' ')+'…';
      }
      line.appendChild(   document.createTextNode( (nr.length?nr[0].nodeValue+' ':'') + name )   );
      line.href='#';
      line.className= 'object' + (isInactive?' inactive':'') ;
      line.onclick=onclick;
      line.objXML=objXML;
      line.catXML=catXML;
      line.rubXML=rubXML;
      elem.appendChild(line);
      var tourId=objXML.getAttribute('tour');
      var texts= map.hasFilledTour(objXML) ? areaTexts : tourTexts ;
      if(tourId) {
        var tour=document.createElement('a');
        tour.className= 'tour' + (isInactive?' inactive':'') ;
        tour.appendChild(   document.createTextNode( texts[map.tours.isVisible(tourId)] )   );
        tour.href='#';
        tour.objXML=objXML;
        tour.onclick=tourOnclick;
        elem.appendChild(tour);
      }
    }
    elem.onmouseover=onmouseover;
    elem.onmousedown=onmousedown;
    elem.onmouseout=onmouseout;
    self.position();
    map.htmlElems.icons.appendChild(elem);
  }
  function clearTimeout() {
    if(!timeout) return;
    window.clearTimeout(timeout);
    timeout=null;
  }
  function hideTimeout() {
    if( elem && elem.parentNode ) elem.parentNode.removeChild(elem);
    elem=null; poiGroup=null;
    timeout=null;
  }
  self.position=function() {
    if(!elem) return;
    htmlPos=map.posScaleToHTML( map.scales.posRefToScale(pos) );
    var style=elem.style;
    if( htmlPos[0]<map.size[0]/2 ) {
      style.left=Math.round(4+htmlPos[0])+'px';
      style.right='auto';
    } else {
      style.left='auto';
      style.right=Math.round(4+map.size[0]-htmlPos[0])+'px';
    }
    if( htmlPos[1]<map.size[1]/2 ) {
      style.top=Math.round(4+htmlPos[1])+'px';
      style.bottom='auto';
    } else {
      style.top='auto';
      style.bottom=Math.round(4+map.size[1]-htmlPos[1])+'px';
    }
  }
  function onmouseover(e) { clearTimeout(); e||(e=window.event); return HelperFunctions.stopPropagation(e); }
  function onmousedown(e) { e||(e=window.event); return HelperFunctions.stopPropagation(e); }
  function onmouseout(e) { timeout=window.setTimeout(hideTimeout, 500); }
  var hide=self.hide=function(givenPOIGroup) {
    if(poiGroup==givenPOIGroup) {
      clearTimeout();
      timeout=window.setTimeout(hideTimeout, 500);
    }
  }
  function onclick(e) {
    e||(e=window.e);
    var uri=map.getVCardURI(this);
    if(uri) map.onVCardClick(uri);
    return HelperFunctions.stopPropagation(e);
  }
  function tourOnclick(e) {
    e||(e=window.event);
    var tourId=this.objXML.getAttribute('tour');
    if( map.tours.isVisible(tourId) ) map.tours.remove(tourId);
    else map.tours.add(tourId);
    var texts= map.hasFilledTour(this.objXML) ? areaTexts : tourTexts ;
    this.firstChild.nodeValue=texts[map.tours.isVisible(tourId)];
    return HelperFunctions.stopPropagation(e);
  }
}





//******************************
//*** class InfoBox          ***
//******************************
function InfoBox(map) {
  var self=this
      , elem=null
      , pos=null
      , htmlPos=null
      , poiGroup=null
      , regexp={hidden:/(^|\s+)hidden(\s+|$)/}
  ;
  function comparePOIs(poi, poi_) { return poi.objXML.getAttribute('name')>poi_.objXML.getAttribute('name'); }
  self.add=function(givenPos, givenPOIGroup) {
    if( map.winCtrl.config.layout.tooltip ) return;
    self.rem();
    if(givenPos) pos=givenPos;
    if(givenPOIGroup) poiGroup=givenPOIGroup;
    if(!pos || !poiGroup) return;
    elem=document.createElement('div');
    elem.id='infobox';
    if(displayCnt<0) elem.className+=' hidden';
    elem.onmousedown=onmousedown;
    elem.onmouseover=onmouseover;
    elem.onmouseout=onmouseout;
    var close=document.createElement('div');
    close.className='close';
    close.onclick=self.rem;
    elem.appendChild(close);
    var pois=document.createElement('div');
    pois.className='pois';
    var poisArray=[];
    for(var i=0; i<poiGroup.length; i++) poisArray.push(poiGroup[i]);
    poisArray.sort(comparePOIs);
    for(var i=0; i<poisArray.length; i++) pois.appendChild( createPOI(poisArray[i]) );
    elem.appendChild(pois);
    self.position();
    map.htmlElems.icons.appendChild(elem);
  }
  self.redraw=function() {
    self.add(pos, poiGroup);
  }
  self.position=function() {
    if(!elem) return;
    htmlPos=map.posScaleToHTML( map.scales.posRefToScale(pos) );
    elem.style.left=(Math.round( htmlPos[0] ))+'px';
    elem.style.top= (Math.round( htmlPos[1] ))+'px';
  }
  self.rem=function() {
    if(elem) {
      elem.parentNode.removeChild(elem);
      elem=null;
      pos=null;
    }
  }
  var displayCnt=0;
  self.hide=function() { if(displayCnt--==0 && elem) elem.className+=' hidden'; };
  self.show=function() { if(++displayCnt==0 && elem) elem.className=elem.className.replace(regexp.hidden, '$2'); };
  var onmouseover, onmouseout; (function() {
    var min=[2, 2]
        , max=[298, 203]
        , moved
        , interval
    ;
    function clrInt() { window.clearInterval(interval); interval=null; }
    function setInt() { interval=window.setInterval(slide, 50); }
    function slide() {
      if(!elem) {
        clrInt();
        return;
      }
      var t= moved ? [moved[0], moved[1]] : [htmlPos[0], htmlPos[1]]
          , p=[parseInt(elem.style.left), parseInt(elem.style.top)]
      ;
      if( Math.abs(t[0]-p[0])<2 && Math.abs(t[1]-p[1])<2 ) {
        clrInt();
      } else {
        elem.style.left=Math.round((p[0]+t[0])/2)+'px';
        elem.style.top=Math.round((p[1]+t[1])/2)+'px';
      }
    }
    onmouseover=function(e) {
      if( min[0]>htmlPos[0] || htmlPos[0]>map.size[0]-max[0] || min[1]>htmlPos[1] || htmlPos[1]>map.size[1]-max[1] ) {
        moved=[
          Math.max(min[0], Math.min(htmlPos[0], map.size[0]-max[0]))
          , Math.max(min[1], Math.min(htmlPos[1], map.size[1]-max[1]))
        ];
        if(!interval) setInt();
      }
    }
    onmouseout=function(e) {
      if(!moved) return;
      moved=null;
      if(!interval) setInt();
    }
  })();
  function onmousedown(e) {
    e||(e=window.event);
    return HelperFunctions.stopPropagation(e);
  }
  function poiOnclick(e) {
    e||(e=window.e);
    var uri=map.getVCardURI(this);
    if(uri) map.onVCardClick(uri);
    return HelperFunctions.stopPropagation(e);
  }
  var
    tourTexts={'false':map.winCtrl.config.lang['infobox.tour.show'], 'true':map.winCtrl.config.lang['infobox.tour.hide']}
    , areaTexts={'false':map.winCtrl.config.lang['infobox.area.show'], 'true':map.winCtrl.config.lang['infobox.area.hide']}
  ;
  function tourOnclick(e) {
    e||(e=window.event);
    var tourId=this.objXML.getAttribute('tour');
    if( map.tours.isVisible(tourId) ) map.tours.remove(tourId); else map.tours.add(tourId);
    var texts= map.hasFilledTour(this.objXML) ? areaTexts : tourTexts ;
    this.firstChild.nodeValue=texts[map.tours.isVisible(tourId)];
    return HelperFunctions.stopPropagation(e);
  }
  function createPOI(poi) {
    var poiHTML=document.createElement('div')
        , objXML=poi.objXML
        , catIds=map.pois.getActiveCatIds(objXML)
        , catXML=map.evalXPath(objXML, '//tmb:category[@id=\''+( catIds.length==1 ? catIds[0] : objXML.getAttribute('cat') )+'\']')[0]
        , rubXML=map.evalXPath(objXML, '//tmb:rubric[@id=\''+objXML.getAttribute('rub')+'\']')[0]
        , imgs=map.evalXPath(objXML, './/tmb:image[1]')
        , isClickable=true;
    ;
    poiHTML.className='poi';
    poiHTML.objXML=objXML;
    isClickable&= !(0==objXML.getAttribute('id').indexOf('x')) || null!=map.getVCardURI(poiHTML) ;

    if(catXML) {
      poiHTML.catXML=catXML;
      poiHTML.rubXML=rubXML;
      if( 1<catIds.length ) poiHTML.className+=' multi';
      else poiHTML.style.backgroundImage='url('+catXML.getAttribute('icon')+')';
      if( map.icons.isInactive(objXML, catXML) ) poiHTML.className+=' inactive';
    }
    if( objXML.getAttribute('icon') ) poiHTML.style.backgroundImage='url('+objXML.getAttribute('icon')+')';

    var nr;
    if( (nr=map.evalXPath(objXML, '@*[local-name()="nr"]')).length ) {
      var text=document.createElement('div');
      text.className='text';
      text.appendChild( document.createTextNode(nr[0].nodeValue) );
      poiHTML.appendChild( text );
    }
    
    if(imgs.length) {
      var img=document.createElement('img');
      img.src=imgs[0].getAttribute('url');
      poiHTML.appendChild(img);
    }
    poiHTML.appendChild( document.createTextNode(
      objXML.getAttribute('name')
      + ( isClickable ? '' : ' ('+map.winCtrl.config.lang['infobox.noinfo']+')' )
    ) );
    if(isClickable) {
      poiHTML.onclick=poiOnclick;
      poiHTML.className+=' clickable';
      var more=document.createElement('div');
      more.className='more';
      more.appendChild( document.createTextNode(map.winCtrl.config.lang['infobox.more']) );
      poiHTML.appendChild(more);
    }
    var tourId=objXML.getAttribute('tour');
    var texts= map.hasFilledTour(objXML) ? areaTexts : tourTexts ;
    if(tourId) {
      var tour=document.createElement('div');
      tour.className='tour more';
      tour.objXML=objXML;
      tour.appendChild(   document.createTextNode( texts[map.tours.isVisible(tourId)] )   );
      tour.onclick=tourOnclick;
      poiHTML.appendChild(tour);
    }
    return poiHTML;
  }
}






// ******************************
// *** class State            ***
// ******************************
function State() {
  var self=this;
  var state={};
  self.set=function(name) { state[name]=true; }
  self.clear=function(name) { delete state[name]; }
  self.contains=function() {
    for(var i=0; i<arguments.length; i++) if( state[ arguments[i] ] ) return true;
    return false;
  }
}






// ******************************
// *** class Tiles            ***
// ******************************
function Tiles(map) {
  var self=this;

  var gridSize=1<<8;
  self.getGridSize=function() { return gridSize; }

  self.tiles={};
  self.coords2pos=function(x, y) { return Math.round(x)+','+Math.round(y); }
  self.pos2coords=function(pos) { pos=pos.split(','); return [parseInt(pos[0]), parseInt(pos[1])]; }

  self.create=function(x, y) {
    var pos=self.coords2pos(x, y)
        , tile=self.tiles[pos]
    ;
    if(tile) {
      clearRemoveTimeout(x, y);
      return;
    }
    var gfx=document.createElement('div')
        , icons=document.createElement('div')
        , styleGfx=gfx.style
        , styleIcons=icons.style
    ;
    gfx.className='gfx';
    styleGfx.width=styleGfx.height=(gridSize)+'px';
    icons.className='icons';
    styleIcons.width=styleIcons.height=(gridSize)+'px';
    self.tiles[pos]={gfx:gfx, icons:icons};
    setCreateTimeout(x, y);
    map.htmlElems.tiles.appendChild(gfx);
    map.htmlElems.icons.appendChild(icons);
  }
  function createTimeout(x, y) {
    var tile=self.tiles[ self.coords2pos(x, y) ];
    if(!tile) return;
    var sE=tile.gfx.style;
    delete tile.timeoutLoad;
    sE.backgroundImage='url('
      +'tiles/image.php'
      +'?image='+map.scales.get().image
      +'&cs=jsmap&bbox='+x+','+y+','+(x-(-gridSize))+','+(y-(-gridSize))
      +'&width='+gridSize+'&height='+gridSize
      +'&format=jpg&quality=85'
      +map.winCtrl.getExtraQuery()
    +')';
    map.icons.create(x, y);
  }
  function setCreateTimeout(x, y) {
    var tile=self.tiles[ self.coords2pos(x, y) ];
    if(!tile) return;
    clearCreateTimeout(x, y);
    tile.timeoutLoad=window.setTimeout(
      (function() { return function() { return createTimeout(x, y); } })()
      , 1000-Math.round( -1000*Math.random() )
    );
  }
  function clearCreateTimeout(x, y) {
    var tile=self.tiles[ self.coords2pos(x, y) ];
    if(!tile) return;
    if(tile.timeoutLoad) {
      window.clearTimeout(tile.timeoutLoad);
      delete tile.timeoutLoad;
    }
  }

  self.remove=function(x, y) { setRemoveTimeout(x, y); }
  self.removeAll=function() {
    for(var pos in self.tiles) {
      pos=self.pos2coords(pos);
      removeTimeout(pos[0], pos[1]);
    }
  }
  function removeTimeout(x, y) {
    var pos=self.coords2pos(x, y)
        , tile=self.tiles[pos]
    ;
    if(!tile) return;
    clearCreateTimeout(x, y);
    clearRemoveTimeout(x, y);
    map.htmlElems.tiles.removeChild( tile.gfx );
    map.htmlElems.icons.removeChild( tile.icons );
    delete self.tiles[pos];
  }
  function setRemoveTimeout(x, y) {
    var tile=self.tiles[ self.coords2pos(x, y) ];
    if(!tile) return;
    clearRemoveTimeout(x, y);
    tile.timeoutRemove=window.setTimeout(
      (function() { return function() { return removeTimeout(x, y); } })()
      , 10
    );
  }
  function clearRemoveTimeout(x, y) {
    var tile=self.tiles[ self.coords2pos(x, y) ];
    if(!tile) return;
    if(tile.timeoutRemove) {
      window.clearTimeout(tile.timeoutRemove);
      delete tile.timeoutRemove;
    }
  }

  self.check=function() {
    var w=map.size[0]
        , h=map.size[1]
        , minX=Math.floor( -map.pos[0]-(w>>1) )&-gridSize
        , maxX=minX-( -w-gridSize )
        , minY=Math.floor( -map.pos[1]-(h>>1) )&-gridSize
        , maxY=minY-( -h-gridSize )
    ;
    for(var x=minX; x<maxX; x-=-gridSize) for(var y=minY; y<maxY; y-=-gridSize) self.create(x, y);
    for(var pos in self.tiles) {
      pos=self.pos2coords(pos);
      if( !(minX<=pos[0] && pos[0]<maxX && minY<=pos[1] && pos[1]<maxY) ) self.remove(pos[0], pos[1]);
    }
  }

}






// ******************************
// *** class Icons            ***
// ******************************
function Icons(map) {
  var self=this;

  var activeIcons=[];
  self.getActiveIcons=function() { return activeIcons; }

  var iconRadius=17;


  function meanPosOfPOIGroup(poiGroup) {
    var pos=[0, 0];
    for(var poi, i=0; poi=poiGroup[i]; i++) {
      pos[0]-=-poi.pos[0];
      pos[1]-=-poi.pos[1];
    }
    pos[0]=pos[0]/i;
    pos[1]=pos[1]/i;
    return pos;
  }
  function onmouseover(e) { map.tooltip.show(meanPosOfPOIGroup(this.poiGroup), this.poiGroup); }
  function onmouseout(e) { map.tooltip.hide(this.poiGroup); }
  function onmousedown(e) {
    if(!e) e=window.event;
    var activeIcons=map.icons.getActiveIcons();
    for(var i; i=activeIcons.pop(); ) i.className=i.className.replace(/(^|\s+)active(\s+|$)/, '$2');
    activeIcons.push(this);
    this.className+=' active';
    map.pois.clearActive();
    var poi, i;
    for(i=0; poi=this.poiGroup[i]; i++) map.pois.setActive(poi._id);
    if( i==1 ) {
      poi=this.poiGroup[0];
      if( i=poi.objXML.getAttribute('tour') ) map.tours.add(i);
      if( map.winCtrl.config.user=='ic' ) {
        this.objXML=poi.objXML;
        this.rubXML=map.evalXPath(poi.objXML, '//tmb:rubric[@id=\''+poi.objXML.getAttribute('rub')+'\']')[0];
        var uri=map.getVCardURI(this);
        if(uri) map.onVCardClick(uri);
      }
    }
    var pos=meanPosOfPOIGroup(this.poiGroup);
    map.infoBox.add(pos, this.poiGroup);
    pos=map.scales.posRefToScale(pos);
    map.setIntendedGeoPosition( map.scales.posScaleToGeo(pos) );
    map.moveToScalePosition( pos );
    return HelperFunctions.stopPropagation(e);
  }

  self.create=function(x, y) {
    var tiles=map.tiles
        , scales=map.scales
        , pois=map.pois
        , tile=tiles.tiles[ tiles.coords2pos(x, y) ]
    ;
    if(!tile) return;
    for(var icon; icon=tile.icons.childNodes[0]; tile.icons.removeChild(icon));
    var p=scales.posScaleToRef([x, y])
        , gridSize=tiles.getGridSize()
        , g=scales.posScaleToRef([-gridSize, -gridSize])
        , minX=p[0]
        , maxX=p[0]-g[0]
        , minY=p[1]
        , maxY=p[1]-g[1]
        , poisInRange=pois.getByRange(minX, maxX, minY, maxY)
        , poiGroups={}
    ;
    for(var i=0; i<poisInRange.length; i++) {
      var poi=poisInRange[i];
      poi.pos_=scales.posRefToScale(poi.pos);
      poi.pos_[0]-=x;
      poi.pos_[1]-=y;
      var p= (poi.types.search?'x':'') + (poi.pos_[0]>>5) + ',' + (poi.pos_[1]>>5) ; // searched pois are grouped only with its own kind
      if(!poiGroups[p]) poiGroups[p]=[];
      poiGroups[p].push(poi);
    }
    var grpCnt=0; // count groups to process
    for(var p in poiGroups) {
      var group=poiGroups[p]
          , icon=document.createElement('div')
          , marker=document.createElement('div')
          , styleIcon=icon.style
          , left=group[0].pos_[0]
          , top=group[0].pos_[1]
          , isActive=pois.isActive( group[0]._id )
          , setInactive=true
          , title=''
      ;
      styleIcon.zIndex=(grpCnt++)+( 'x'==p.charAt(0) ? poisInRange.length : 0 ); // put groups of searched POIs on top of ordinary POIs
      group[0].icon=icon;
      for(var i=1; i<group.length; i++) {
        group[i].icon=icon;
        isActive|=pois.isActive( group[i]._id );
        left-=-group[i].pos_[0];
        top-=-group[i].pos_[1];
      }
      left/=group.length;
      top/=group.length;
      icon.poiGroup=poiGroups[p];
      icon.map=map;
      icon.onmouseover=onmouseover;
      icon.onmouseout=onmouseout;
      icon.onmousedown=onmousedown;
      icon.className='icon'+( isActive ? ' active' : '' );
      marker.className='marker';
      icon.appendChild( marker );
      if(isActive) activeIcons.push(icon);
      styleIcon.left=( Math.round( left-iconRadius ) )+'px';
      styleIcon.top=( Math.round( top-iconRadius ) )+'px';
      var objXML, catIds;
      for(var i=0; i<group.length; i++) {
        objXML=group[i].objXML;
        catIds=pois.getActiveCatIds(objXML);
        if( !catIds.length ) setInactive=false;
        else for(var j=0; j<catIds[j]; j++) setInactive&=self.isInactive(objXML, pois.getCatXML(catIds[j]));
        title+=objXML.getAttribute('name')+';\r\n';
      }
      var iconSrc=null;
      if( i>1 || catIds.length>1 ) {
        icon.className+=' multi';
      } else if( objXML.getAttribute('icon') ) {
        iconSrc=objXML.getAttribute('icon');
      } else if(catIds.length) {
        iconSrc=pois.getCatXML(catIds[0]).getAttribute('icon');
      }
      var nr;
      if( i==1 && (nr=map.evalXPath(objXML, '@*[local-name()="nr"]')).length ) {
        var text=document.createElement('div');
        text.className='text';
        text.appendChild( document.createTextNode(nr[0].nodeValue) );
        icon.appendChild( text );
        icon.className+=' blank';
      }
      if(iconSrc) styleIcon.backgroundImage='url('+iconSrc+')';
      if(setInactive) icon.className+=' inactive';
      if( !map.winCtrl.config.layout.tooltip ) icon.title=title.substring(0, title.length-3);
      tile.icons.appendChild(icon);
    }
  }

  self.redraw=function() {
    var tiles=map.tiles;
    for(var pos in tiles.tiles) {
      pos=tiles.pos2coords(pos);
      self.create(pos[0], pos[1]);
    }
  }

  var activeClas={};
  self.setActiveClis=function(activeClasToBeSet, activeClisToBeSet) {
    activeClas={};
    for(var claId in activeClasToBeSet) {
      activeClas[claId]={};
      for(var cliId in activeClisToBeSet) if(claId==activeClisToBeSet[cliId].getAttribute('cla')) activeClas[claId][cliId]=true;
    }
    self.redraw();
  }
  self.isInactive=function(objXML, catXML) {
    if(!catXML) catXML=map.pois.getCatXML(objXML);
    if(catXML) {
      var isInactive=false
          , cla=objXML.getAttribute('cla')
          , cli=objXML.getAttribute('cli')
      ;
      cla= cla ? ' '+cla+' ' : '';
      cli= cli ? ' '+cli+' ' : '';
      for(var claId in activeClas) if( cla.indexOf(' '+claId+' ')>=0 ) {
        for(var cliId in activeClas[claId]) if( cli.indexOf(' '+cliId+' ')<0 ) {
          isInactive=true;
          break;
        } 
      }
    }
    return isInactive;
  }

}






// ******************************
// *** class Scales           ***
// ******************************
function Scales(map) {
  var self=this;

/*
  var refPos=[3366512.74-10, 5812818.86]
      , refSize=[237958.49, 247707.53]
      , refFactor=[6.614, -6.614]
      , scales=self.scales=[
            {image:'3000000',  width:  150, height:  157}
          , {image:'1600000',  width:  281, height:  293}
          , {image:'750000',   width:  600, height:  624}
          , {image:'500000',   width:  900, height:  937}
          , {image:'250000',   width: 1799, height: 1873}
			    , {image:'100000',   width: 4497, height: 4682}
			    , {image:'050000',   width: 8995, height: 9363}
			    , {image:'025000',   width:17989, height:18726}
        ]
  ;
*/
  var refPos=[            (3209784+3506871)/2 ,       (5654327+5948107)/2 ]
      , refSize=[ Math.abs(3209784-3506871) , Math.abs(5654327-5948107) ]
      , refFactor=[6.614, -6.614]
      , scales=self.scales=[
            {image:'3000', width:  374/2, height:  370/2}
          , {image:'1600', width:  702/2, height:  694/2}
          , {image:'0750', width: 1497/2, height: 1482/2}
          , {image:'0500', width: 2246/2, height: 2221/2}
          , {image:'0250', width: 4492/2, height: 4442/2}
          , {image:'0100', width:11230/2, height:11105/2}
          , {image:'0050', width:22460/2, height:22210/2}
          , {image:'0025', width:44918/2, height:44418/2}
        ]
  ;

  switch(map.winCtrl.config.user) {
  case 'ic':
    scales.shift();
    break;
  }
  
  self.getMaxGeoBBox=function(shrink) {
    if(!shrink) shrink=1;
    return [
        [refPos[0]-refSize[0]*shrink/2, refPos[1]+refSize[1]*shrink/2]
      , [refPos[0]+refSize[0]*shrink/2, refPos[1]-refSize[1]*shrink/2]
    ];
  };
  self.getGeoBBox=function() {
    return [
        [(refPos[0]-( refSize[0]-refPos[0]))>>1, (refPos[1]-( refSize[1]-refPos[1]))>>1]
      , [(refPos[0]-(-refSize[0]-refPos[0]))>>1, (refPos[1]-(-refSize[1]-refPos[1]))>>1]
    ]
  }
  
  self.findFittest=function(geoSize) {
    var sclNr=0
        , size=map.size
    ;
    for(var i=scales.length; i-->0; ) {
      var f=calcFactor(i)
          , s=self.scaleScaleToGeo(size, f)
      ;
      if(geoSize[0]<s[0] && geoSize[1]<s[1]) {
        sclNr=i;
        break;
      }
    }
    return sclNr;
  }

  self.current=0;
  self.get=function(sclNr) {
    if(sclNr==undefined) sclNr=self.current;
    return scales[sclNr];
  }

  var factor=null;
  function calcFactor(sclNr) {
    var scale=self.get(sclNr);
    return factor=[ refSize[0]/refFactor[0]/2/scale.width , refSize[1]/refFactor[1]/-2/scale.height ];
  }
  self.getFactor=function() { return factor; };

  self.posGeoToRef=function(pos) { return [(pos[0]-refPos[0])/refFactor[0], (pos[1]-refPos[1])/refFactor[1]]; }
  self.posRefToGeo=function(pos) { return [refFactor[0]*pos[0]-(-refPos[0]), refFactor[1]*pos[1]-(-refPos[1])]; }

  self.posScaleToRef=function(pos) { return [pos[0]*factor[0], pos[1]*factor[1]]; }
  self.posRefToScale=function(pos) { return [pos[0]/factor[0], pos[1]/factor[1]]; }

  self.scaleScaleToGeo=function(pos, f) {
    if(f==undefined) f=factor;
    return [pos[0]*f[0]*refFactor[0], pos[1]*f[1]*-refFactor[1]];
  }

  self.posGeoToScale=function(pos) { return self.posRefToScale(self.posGeoToRef(pos)); }
  self.posScaleToGeo=function(pos) { return self.posRefToGeo(self.posScaleToRef(pos)); }

  self.switchTo=function(sclNr) {
    self.current=sclNr;
    calcFactor();
  }

  calcFactor();

}






// ******************************
// *** class POIs             ***
// ******************************
function POIs(map) {
  var self=this;

  var pois={}, poisByRange=[], poisActive={}, dirty=false;
  var calc_id=self.calc_id=function(objXML, duplicate) {
    var id=objXML.getAttribute('id');
    return '_'+id+( duplicate===undefined ? '' : '.'+duplicate );
  };
  self.getByObjXML=function(objXML) { return pois[ calc_id(objXML) ]; };
  self.getById=function(id) { return pois[ '_'+id ]; };
  function insert(pos, objXML, type, duplicate) {
    var _id=calc_id(objXML, duplicate)
        , i=calcRangeIndices(pos)
        , catids=objXML.getAttribute('cats')
    ;
    if( catids && (catids=catids.split(' ')) ) for(var j=0; j<catids.length; j++) if(!categories[ catids[j] ])
      categories[ catids[j] ]=map.evalXPath(objXML.ownerDocument.documentElement, 'tmb:category[@id='+catids[j]+']')[0];
    if(!pois[_id]) {
      pois[_id]={_id:_id, objXML:objXML, pos:pos, pos_:[0, 0], types:{}, icon:null};
      try {
        poisByRange[ i[0] ][ i[1] ].push( pois[_id] );
      } catch(e) {
        if( !poisByRange[ i[0] ] )        poisByRange[ i[0] ]=[];
        if( !poisByRange[ i[0] ][ i[1]] ) poisByRange[ i[0] ][ i[1] ]=[];
        poisByRange[ i[0] ][ i[1] ].push( pois[_id] );
      }
    }
    var types=pois[_id].types;
    if(!types[type]) types[type]=1; else types[type]++;
    return _id;
  }
  function remove(_id, type) {
    var poi=pois[_id], types=poi.types;
    if( !poi || !types[type] || --types[type]>0 ) return;
    delete types[type];
    for(var t in types) return;
    var i=calcRangeIndices(poi.pos), rng=poisByRange[ i[0] ][ i[1] ];
    for(var j=0; j<rng.length; j++) if(rng[j]==poi) {
      rng.splice(j, 1);
      break;
    }
    delete pois[_id];
    delete poisActive[_id];
  }

  function redraw() {
    map.icons.redraw();
    map.infoBox.rem();
  }

  self.getActive=function() { return poisActive(); }
  self.setActive=function(_id) { poisActive[_id]=pois[_id]; }
  self.isActive=function(_id) { return undefined!=poisActive[_id]; }
  self.clearActive=function() { poisActive={}; }

  var rangeBits=8;
  function calcRangeIndices(pos) { return [pos[0]>>rangeBits, pos[1]>>rangeBits]; }
  self.getByRange=function(minX, maxX, minY, maxY) {
    var result=[];
    for(  var rngIndX=minX>>rangeBits
              , rngIndMaxX=maxX>>rangeBits
          ; rngIndX<=rngIndMaxX
          ; rngIndX++
    ) if(poisByRange[rngIndX]) {
      for(  var rngIndY=minY>>rangeBits
                , rngIndMaxY=maxY>>rangeBits
                , rng
            ; rngIndY<=rngIndMaxY
            ; rngIndY++
      ) if(rng=poisByRange[rngIndX][rngIndY]) {
        for(var i=0, poi; poi=rng[i], i<rng.length; i++) {
          var poi=rng[i], x=poi.pos[0], y=poi.pos[1];
          if( minX<=x && x<maxX && minY<=y && y<maxY ) result.push(poi);
        }
      }
    }
    return result;
  }

  var categories={};
  self.getCatXML=function(param) { return categories[ typeof param=='object' ? param.getAttribute('cat') : param ]; }

  var activeCategoryIds={};
  self.getActiveCatIds=function(objXML) {
    var actCats=[], objCats=objXML.getAttribute('cats');
    if(objCats && (objCats=objCats.split(' ')).length ) for(var i=0; i<objCats.length; i++) activeCategoryIds[ objCats[i] ] && actCats.push(objCats[i]);
    actCats.length || (objCats=objXML.getAttribute('cat')) && actCats.push(objCats);
    return actCats;
  }
  
  self.addGroup=function(data, callback, type, dontChangeView) {
    var poisXML;
    if( typeof data=='string' ) {
      type='cat';
      data=data.split(',');
      var query='';
      for(var i=0; i<data.length; i++) if( data[i]=parseInt(data[i]) ) query+='&catid[]='+data[i];
      var xmlReq=HelperFunctions.getXMLHttpRequest();
      xmlReq.open('GET', 'xml.php?dontGroup&orderby=name'+query+map.winCtrl.getExtraQuery(), false);
      try { xmlReq.send(null); } catch(e) {}
      if( xmlReq.readyState==4 && xmlReq.status==200 ) {
        for(var i=0; i<data.length; i++) activeCategoryIds[ data[i] ]=true;
        poisXML=xmlReq.responseXML.documentElement;
      }
    } else if(data.documentElement) {
      poisXML=data.documentElement;
    }
    if(poisXML) {
      var catPOIs=map.evalXPath(poisXML, 'tmb:object[tmb:coords]')
          , obj, minPos, maxPos
      ;
      function _insert(coords, duplicate) {
        var i= duplicate===undefined ? 0 : duplicate
            , x=parseFloat( coords[i].getAttribute('x') )
            , y=parseFloat( coords[i].getAttribute('y') )
            , pos=map.scales.posGeoToRef([x, y])
        ;
        if(!minPos) minPos=[x, y];
        if(!maxPos) maxPos=[x, y];
        if(x<minPos[0]) minPos[0]=x; else if(x>maxPos[0]) maxPos[0]=x;
        if(y<minPos[1]) minPos[1]=y; else if(y>maxPos[1]) maxPos[1]=y;
        insert(pos, obj, type, duplicate);
      }
      for(var i=0; obj=catPOIs[i]; i++) {
        _insert( map.evalXPath(obj, 'tmb:coords') );
        for(var j=0, coordsExtra=map.evalXPath(obj, 'tmb:coords-extra/tmb:coords'); j<coordsExtra.length; j++) {
          _insert(coordsExtra, j);
        }
      }
      redraw();
      if(!(dontChangeView||type=='cat'||type=='event')&&minPos&&maxPos) map.moveToBBox(minPos, maxPos);
    }
    if(callback) callback();
  }
  self.removeGroup=function(data, type) {
    if(typeof data=='string') {
      type='cat';
      for(var _id in pois) {
        delete activeCategoryIds[data];
        var poi=pois[_id], cats=' '+poi.objXML.getAttribute('cats')+' ';
        if( data==poi.objXML.getAttribute('cat') || 0<cats.indexOf(' '+data+' ') ) remove(_id, type);
      }
    } else if(data.documentElement) {
      var objXMLs=map.evalXPath(data.documentElement, '//tmb:object[tmb:coords]');
      for(var i=0, objXML; objXML=objXMLs[i]; i++) {
        remove( calc_id(objXML) , type );
        for(var j=0, coordsExtra=map.evalXPath(objXML, 'tmb:coords-extra/tmb:coords'); j<coordsExtra.length; j++) {
          remove( calc_id(objXML, j) , type );
        }
      }
    }
    redraw();
  }


}






//******************************
//*** class Tours            ***
//******************************
function Tours(map) {
  var self=this;
  var nameSpaces={svg:'http://www.w3.org/2000/svg', vml:'urn:schemas-microsoft-com:vml', xlink:'http://www.w3.org/1999/xlink'};

  function setAttributes(target, attrs) {
    if(target.setAttribute) for(var k in attrs) target.setAttribute(k, attrs[k]);
    else for(var k in attrs) target[k]=attrs[k];
  }

  var last={size:[null, null], pos:[null, null], f:[null, null]};
  function checkChanges() {
    var changes=0;
    if( last.size[0]!=map.size[0] || last.size[1]!=map.size[1] ) {
      last.size=[map.size[0], map.size[1]];
      changes|=4;
    }
    if( last.pos[0]!=map.pos[0] || last.pos[1]!=map.pos[1] ) {
      last.pos=[map.pos[0], map.pos[1]];
      changes|=2;
    }
    var f=map.scales.getFactor();
    if( last.f[0]!=f[0] || last.f[1]!=f[1] ) {
      last.f=[f[0], f[1]];
      changes|=1;
    }
    return changes;
  }
  self.position=function() {}; // defined later according to type (vml or svg)

  var maxPathLenBitMask=-1; // formerly used to fragment path, not possible any more, because the whole path may be drawn as filled area
  var drawTour; // is a function, defined later according to type (vml or svg)

  switch( map.winCtrl.config.vector ) {
  case 'vml':
    var vml=document.createElement('div');
    setAttributes(vml, {id:'jsMap-tours'});
    var group=document.createElement('vml:group');
    setAttributes(group, {coordSize:'256,256'});
    setAttributes(group.style, {width:'256px', height:'256px'});
    vml.appendChild(group);
    document.getElementsByTagName('body')[0].appendChild(vml);
    self.position=function() {
      var changes=checkChanges();
      if(changes&4) setAttributes(vml.style, {width:last.size[0]+'px', height:last.size[1]+'px'});
      if(changes&6) setAttributes(group.style, {left:((last.size[0]>>1)+last.pos[0])+'px', top:((last.size[1]>>1)+last.pos[1])+'px'});
      if(changes&1) setAttributes(group, {coordSize:[256*last.f[0], 256*last.f[1]]});
    }
    drawTour=function(routeXML) {
      var tour=document.createElement('vml:group')
          , color=routeXML.documentElement.getAttribute('color')
          , fill=routeXML.documentElement.getAttribute('fill') ? 'true' : 'false'
      ;
      setAttributes(tour, {coordSize:'256,256'});
      setAttributes(tour.style, {width:'256px', height:'256px'});
      var path, shape, shapeTmpl=document.createElement('vml:shape');
      setAttributes(shapeTmpl, {coordSize:'256,256', filled:fill, fillColor:color, strokeColor:color, strokeWeight:'4pt'});
      setAttributes(shapeTmpl.style, {left:0, top:0, width:'256px', height:'256px'});
      shapeTmpl.appendChild( document.createElement('vml:stroke') );
      setAttributes(shapeTmpl.lastChild, {opacity:.75});
      shapeTmpl.appendChild( document.createElement('vml:fill') );
      setAttributes(shapeTmpl.lastChild, {opacity:.5});
      for(var j=0, segs=map.evalXPath(routeXML.documentElement, '/route/seg'); j<segs.length; j++) {
        for(var i=0, segments=map.evalXPath(segs[j], 'segment'); i<segments.length; i++) {
          if( !(i&maxPathLenBitMask) ) {
            if(i) setAttributes(shape, {path:path});
            tour.appendChild( shape=shapeTmpl.cloneNode(true) );
            path='m'+(i ? segments[i-1].getAttribute('p')+' l' : '');
          } else path+=' l';
          path+=segments[i].getAttribute('p');
        }
        if( i&maxPathLenBitMask ) setAttributes(shape, {path:path});
      }
      group.appendChild(tour);
      return tour;
    }
    break;
  case 'svg':
    var svg=document.createElementNS(nameSpaces.svg, 'svg:svg');
    setAttributes(svg, {id:'jsMap-tours', version:'1.1'});
    var groupTranslate=document.createElementNS(nameSpaces.svg, 'svg:g');
    var groupScale=document.createElementNS(nameSpaces.svg, 'svg:g');
    groupTranslate.appendChild(groupScale);
    svg.appendChild(groupTranslate);
    document.getElementsByTagName('body')[0].appendChild(svg);
    self.position=function() {
      var changes=checkChanges();
      if(changes&4) setAttributes(svg, {width:last.size[0], height:last.size[1]});
      if(changes&6) setAttributes(groupTranslate, {transform:'translate('+[(last.size[0]>>1)+last.pos[0], (last.size[1]>>1)+last.pos[1]]+')'});
      if(changes&1) setAttributes(groupScale, {transform:'scale('+[1/last.f[0], 1/last.f[1]]+')', 'stroke-width':6*last.f[0]});
    }
    drawTour=function(routeXML) {
      var tour=document.createElementNS(nameSpaces.svg, 'svg:g')
          , color=routeXML.documentElement.getAttribute('color')
          , fill=routeXML.documentElement.getAttribute('fill') ? color : 'none'
      ;
      setAttributes(tour, {fill:fill, 'fill-opacity':.5, stroke:color, 'stroke-opacity':.75});
      var d, path;
      for(var j=0, segs=map.evalXPath(routeXML.documentElement, '/route/seg'); j<segs.length; j++) {
        for(var i=0, segments=map.evalXPath(segs[j], 'segment'); i<segments.length; i++) {
          if( !(i&maxPathLenBitMask) ) {
            if(i) setAttributes(path, {d:d});
            tour.appendChild( path=document.createElementNS(nameSpaces.svg, 'svg:path') );
            d='M'+(i ? segments[i-1].getAttribute('p')+' L' : '');
          } else d+=' L';
          d+=segments[i].getAttribute('p');
        }
        if(i&maxPathLenBitMask) setAttributes(path, {d:d});
      }
      groupScale.appendChild(tour);
      return tour;
    }
    break;
  }

  var current={};
  function processRoute(routeXML) {
    var segments;
    for(var j=0, segs=map.evalXPath(routeXML.documentElement, '/route/seg'); j<segs.length; j++) {
      if( 'true'==segs[j].getAttribute('close') ) {
        segments=map.evalXPath(segs[j], 'segment[1]');
        if(segments.length) segs[j].appendChild( segments[0].cloneNode(true) );
      }
      segments=map.evalXPath(segs[j], 'segment');
      for(var i=0; i<segments.length; i++) {
        var s=segments[i], p=[ parseInt(s.getAttribute('x')) , parseInt(s.getAttribute('y')) ];
        p=map.scales.posGeoToRef(p);
        s.setAttribute('p', Math.round(p[0])+','+Math.round(p[1]));
      }
    }
    return routeXML;
  }
  function add(id) {
    if(current[id]) current[id].elem.style.display='';
    else {
      map.winCtrl.loader.show();
      window.setTimeout(
        function() {
          var xmlReq=HelperFunctions.getXMLHttpRequest();
          xmlReq.open('GET', 'routeXML.php?id='+id+map.winCtrl.getExtraQuery(), false);
          try { xmlReq.send(null); } catch(e) {}
          if( xmlReq.readyState==4 && xmlReq.status==200 ) {
            var routeXML=processRoute(xmlReq.responseXML);
            current[id]={routeXML:routeXML, elem:drawTour(routeXML)};
            map.infoBox.redraw();
          }
          map.winCtrl.loader.hide();
        }
        , 0
      );
    }
  } self.add=add;
  function isVisible(id) {
    return !(current[id]==undefined || current[id].elem.style.display=='none');
  } self.isVisible=isVisible;
  function remove(id) {
    if(current[id]) current[id].elem.style.display='none';
  } self.remove=remove;
  
}
