// JavaScript Document

// create star icon
// http://chart.apis.google.com/chart?cht=r&chd=t:10,20,10,20,10,20,10,20,10,20,10,20,5&chs=100x100&chco=ff0000&chm=B,ff000080,0,1.0,5.0
// http://code.google.com/apis/chart/styles.html


	var MapMaker = new Class({
		Implements: [Options, Events, Chain],	
		options: {
			
			api: {
				url: 			'http://maps.google.com/maps',
				key:	{	
					local:		'ABQIAAAA72sx3KpMqkKR16NEWQAJZBT2yXp_ZAY8_ufC3CFXhHIE1NvwkxSVQ8HUIVqrbN4ssFIdMZFPAE5nfw', // localhost
					//remote:		'ABQIAAAA72sx3KpMqkKR16NEWQAJZBSVcPVrk1BRzLaZDtFkld6uf5g2nxQsanR6QiL7rdGSMCzztO1TeIr_rQ', // net4visions.com
					remote:		'ABQIAAAA72sx3KpMqkKR16NEWQAJZBQFNj2dqwBFZHwObD7J3urAbaXzcRQFOQQ4w5zVICpye9nB6Y3K1NF6Cw'  // visions4net.com
				},
				options: {
					file: 		'api',
					v: 			'2.x',
					async: 		2
				}
			},
			delay:				500,	// increase this number if you have slow Internet connection
			waiter: 			true,
			demo:				true,	// if true, auto create a few markers 
			map: {
				lat:			37.4419,
				lng:			-122.1419,
				zoom: 			13,
				surl:			'http://maps.google.com/staticmap'
			},			
			sortable: 			true,
			syncOnChange:		true,
			charLimit: 			3,
			requestLocation:	true,
			snapToLocation:		true,	// snap marker to closest address
			requestAltitude: 	false,
			requestTimeout: 	5000,	// fetch geodata timeout
			pointer: 			true, 	// use temporary marker onClick map - not implemented yet
			pointerColor: 		'#33cc00',
			highlight: {
				main:			'#ffffff',
				success:		'#96c251',
				error:			'#ff3300',
				edit:			'#fff2bf',
				remove:			'#ff3300'
			},
			idPrefix:			'mtgt_',
			
			pic_path:			'./images/pictures',
			
			debug: 				true			
		},
		
		initialize: function(options) {
			this.setOptions(options);	
			// todo: error handling !!!!!!!!!!!!!!!
			
			
			// Opera Fix
			// if (Browser.Engine.presto) { navigator.userAgent="Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.2)"; }
			
			if (this.options.debug) dbug.enable(true);
			         
			this.addMarkerForm			= $('addMarkerForm');
			this.geocoderForm			= $('geocoderForm');
			this.frmMarkerSettings		= $('frmMarkerSettings');
			this.frmMapSettings 		= $('frmMapSettings');
			this.previewIcon 			= $('iconPreviewIMG');
			this.mapCanvas				= $('mapCanvas');
			this.markerList				= $('markerListContainer');
			this.directionList			= $('directionListContainer');
			this.panoramaView			= $('panoramaView');
			this.IsHighlighted 			= false;
			this.isSortingMarkers		= false;
			this.markers				= [];
			this.editMode				= false;
			this.togglableSections		= $$('.toggle');
			
			//this.addMarkerForm.reset();
			$('txtImage').set('value', '');
			
			// icon keys
			this.iconKeys = new Hash({
				marker:			['width', 'height', 'primaryColor', 'cornerColor', 'strokeColor'],
				labeledmarker:	['primaryColor', 'strokeColor', 'label', 'labelColor', 'addStar', 'starPrimaryColor', 'starStrokeColor'],
				flat:			['width','height','primaryColor','label','labelSize','labelColor','shape']
			});
			
			
			// chains
			this.chains 				= {
				addMarker:				new Chain(),
				getAltitude:			new Chain(),
				moveMap:				new Chain()
			}
			
			// reset forms
			//this.frmMarkerSettings.reset();
			//this.frmMapSettings.reset();
			this.panoramaView.getParent().hide();
			
			this.setLayout();
			this.loadApi();
			this.toggleVisibleSections();		
			this.attachEvents();
			
			this.handlePolyline();
			//this.requestLocation();
		},
		
		
		// fn: setLayout - tabs, accordions, sortables, colorpickers
		setLayout: function() {			
			
			// ACCORDIONS				
			this.accordions = [];
			$$('dl.accordion').each(function(el, index) {	
				togglers = el.getElements('.toggler');
				sections = el.getElements('.section');	
				
				var parentHeight = el.getParent().getDimensions().height;
				var sectionHeight = parentHeight - ((togglers[0].getDimensions().height) * togglers.length);
				
				// to do: add sections to show on init!!!
				this.accordions.push(
					new Accordion(togglers, sections, {
					 	show: 0,//(index == 1) ? 0 : 1,
						fixedHeight: false,
						onActive: function(toggler, section) {
							toggler.addClass('expanded');
						},
						onBackground: function(toggler, section) {
							toggler.removeClass('expanded');
							if (!index == 1) return false;
							
							// maps accordion
							//if (this.map.overlays.has('panorama')) this.panoramaView.getParent().toggle();
							this.toggleVisibleSections();
						}.bind(this)						
					 })				
				)
			}.bind(this));
			
			
			// TAB CONTAINERS
			var tabs = [];
			$$('div.tabContainer').each(function(el, index) {			
				tabs.push(
					new TabSwapper({
						tabs			: el.getElements('ul.tabs li'),
						sections		: el.getElements('div.panel'),
						selectedClass	: 'on',
						deselectedClass	: 'off',
						disabledClass	: 'disabled',			
						onActive		: $empty, // (index, panel, tab)
						onActiveFx		: $empty, // (index, panel, tab)	
						onBackground	: $empty  // (index, panel, tab)
					})
				);	
			});
			this.tabs = tabs;		
			this.tabs[5].disable(1); // disable directions
			
			
			// SORTABLES			
			if (this.options.sortable) {
				this.hasMoved = false;			
				var fxSort = new Sortables('div#main', {
					constrain	: true,
					clone		: true,
					revert		: true,
					handle		: 'dt.toggler.first',
					onStart		: function() { this.hasMoved = true; }.bind(this),
					onComplete	: function() { if (this.hasMoved) { } } //console.log('sorted to:' + fxSort.serialize(1) +' || '+ this.hasMoved) }.bind(this) 		
				});
				this.sortables = fxSort;
			}
			
			
			// COLORPICKERS
			var CP = new ColorPicker({
				onPick: function(args) { // args contains caller, target, color
					if (!$chk(args) || !$chk(args.target)) return false;									
					this._onChangeColor(args.target, args.color);
				}.bind(this)	
			});
			
			var color, args = {};			
			$$('a.color').each(function(item, index) {
				color = item.getNext().value;
				item.set({
					styles: { 'color': color, 'background-color': color },
					events: {
						'click': function(ev) {
							ev.stop();
							args.caller = item;
							args.target = item.getNext();
							args.colors = colors = (item.get('rel')) ? item.get('rel').split(':')[1] : 'default';							
							if (item.getParent().getParent().hasClass('disabled')) return false;							
							CP.toggle(args);
						}
					}
				});
			});	
			
			
			// TOOLTIPS
			this.tips = new Tips({ fixed: true, className: 'tips' });
			this.tips.addEvent('show', function(tip){ tip.fade( 'in'  ); });
			this.tips.addEvent('hide', function(tip){ tip.fade( 'out' ); });
		},
		
		
		// load map API 
		loadApi: function() {
			var count	= -1;	
			var self  	= this;		
			var error	= true;
			var delay	= this.options.delay;
			
			function failure() {
				dbug.log('failure loading api');
				var html = '<p class="error">Error: Unable to load Google Map API - please try again...</p>'		
				self.mapCanvas.set('html', html);
				if ($defined(this.waiter)) self.waiter.stop();		
				return false; // unable to run application - stop
			}
			
			function success() {
				dbug.log('sucess loading api');
				self.loadMap();
			}
			
			function load() {
				function retry() {
					count++;
					if (count < 20 && error) {
						load.delay(delay);
						return false;					
					}
					
					if (error) failure();
				};
				
				try {				
					if (typeof(GMap2) == 'undefined') {
						error = true;
						retry();
					} else {
						error = false;
						success();
						return false;
					}
				} catch(e) {				
					retry();	
				}			
			}
	
			// load map api
			var api 	= this.options.api;
			var options	= $H(api.options);
			options.key	= (window.location.hostname == 'localhost') ? api.key.local : api.key.remote;
			
			new Asset.javascript(api.url + '?' + options.toQueryString(), {
				onload: function() {				
					if (this.options.waiter) { // show loader
						var el = this.mapCanvas.getParent().getParent();					
						this.waiter = new Waiter(el).start();
					}				
				}.bind(this)
			});		
		
			load();
		},
		
		// load dynamic map	
		loadMap: function() {
			if (!GBrowserIsCompatible()) return false;// browser not compatible
			
			var options = {
				googleBarOptions: 	this.setGoogleBarOptions(),				
				draggableCursor:	'pointer',
				draggingCursor:		'move',
				sensor:				false,
				usageType:			'o',
				suppressCopyright:	true,
				//logoPassive:		true	// doesn't work together with googleBarOptions!!!
			}
			
			var opt		= this.options.map;			
			var lat 	= opt.lat; 
			var lng 	= opt.lng;
			var zoom	= opt.zoom;
			var type	= this.toMapType($('radMapType').get('inputValue'));
			var map 	= new GMap2(this.mapCanvas, options);
		  
			map.setCenter(new GLatLng(lat, lng), zoom, type);
			
			this.map = map;
			this.attachMapEvents();
			
			// init overlays/controls objects
			this.map.overlays = new Hash();
			this.map.controls = new Hash();
			
			this.setMapOptions();
			
			// add events: todo: move to initialize?!			
			this.addEvent('onChangeMap', this.onChangeMap.bind(this));
			
			
			//this.updateMarker();
			this.setDemoMarker();	
						
			// hide waiter
			if (this.waiter) this.waiter.stop();
		},
		
		
		// fn: attachMapEvents
		attachMapEvents: function() {
			GEvent.addListener (this.map, 'load', function() {				
				dbug.log('map is loaded');
				//dbug.log( this.mapCanvas.getElements('div span').length	);
			}.bind(this));
			
			// map clicked
			GEvent.addListener (this.map, 'click', function(overlay, latlng) {
				this.onClickMap(overlay, latlng);
			}.bind(this));

			GEvent.addListener (this.map, 'addoverlay', function(overlay) {															 
				//dbug.log('overlay added to map');
			}.bind(this));

			GEvent.addListener (this.map, 'removeoverlay', function(overlay) {
				//dbug.log('overlay was removed from map');		
			}.bind(this));

			// map type changed
			GEvent.addListener (this.map, 'maptypechanged', function(ev) {
				// to do: remove ctrlOverview if 3D satellite
				if (!this.mapTypeHasChanged) {
					var type = this.mapNameToInt(this.map.getCurrentMapType().getName());	
					$('radMapType').set('inputValue', type);
				}
				this.mapTypeHasChanged = false;
			}.bind(this));
			
			// map moved
			GEvent.addListener(this.map, 'moveend', function() {				
				//dbug.log('moveend map fired');
				this.chains.moveMap.callChain();
				this.fireEvent('onChangeMap');			
			}.bind(this))
			
			// map zoom end
			GEvent.addListener(this.map, 'zoomend', function(currentZoom, newZoom) {			
				$('txtZoom').set('value', newZoom);	
			}.bind(this));	
			
			// infowindow open
			GEvent.addListener(this.map, 'infowindowopen', function() {				
				return false;
			}.bind(this));
			
			// infowindow close
			GEvent.addListener(this.map, 'infowindowbeforeclose', function() {				
				return false;		
			}.bind(this))
			// infowindow close
			GEvent.addListener(this.map, 'infowindowclose', function() {				
				this.onCloseInfoWindow();		
			}.bind(this))
		},
		
		
		// fn: setMapOptions
		setMapOptions: function(formData) {		
			var dat = $defined(formData) ? formData : this.getFormFields(this.frmMapSettings);
			var options = new Hash({				
				mapType:				dat.radMapType,
				mapTypeTerrain:			true,
				mapTypeSatellite3D:		true,
				lockMap:				dat.chkLockMap,
				disableDragging:		dat.chkDisableDragging,
				hideCopyright:			dat.chkHideCopyright,
				hideControls:			dat.chkHideControls,
				showOverview:			dat.chkShowOverview,
				overviewSize:			{width: 150, height: 150},
				showZoom:				dat.chkShowZoom,
				zoomType:				dat.radZoomType,
				showMapType:			dat.chkShowMapType,
				useShortNames:			dat.chkUseShortNames,
				showSearch:				dat.chkShowSearch,
				showScale:				dat.chkShowScale,
				showTraffic:			dat.chkShowTraffic,
				showIncidents:			dat.chkShowTrafficIncidents,
				showStreetview:			dat.chkShowStreetview,
				showPanorama:			dat.chkShowPanorama,
				showWikipedia:			dat.chkShowWikipedia,
				showPanoramio:			dat.chkShowPanoramio
			});
			
			
			// Create object of GLayers
			// alt: com.panoramio.popular
			this.map.layers = new Hash({
				wiki: new GLayer('org.wikipedia.en'),
				pano: new GLayer('com.panoramio.all')
			});
			
			
			// controls and settings			
			if (options.showMapType)			{ this.addMapControl(new GMapTypeControl(options.useShortNames), 'type'); }
												  
										
			if (options.mapTypeTerrain)			{ this.map.addMapType(G_PHYSICAL_MAP); }
			if (options.mapTypeSatellite3D)		{ this.map.addMapType(G_SATELLITE_3D_MAP); }
			
			if (options.showSearch)				{ this.map.enableGoogleBar(); }
			
			if (options.showOverview)			{ if (options.mapType != 2) { var ctrl = this.addMapControl(new GOverviewMapControl(new GSize(options.overviewSize.width, options.overviewSize.height)), 'overview'); 
												  ctrl.setMapType(this.map.getCurrentMapType()); } }
			
			if (options.showZoom) 				{ this.addMapControl(this.toZoomControl(options.zoomType), 'zoom'); }
													  
			if (options.showScale)				{ this.addMapControl(new GScaleControl(), 'scale'); }
												  
			if (options.hideControls)			{ this.map.hideControls();	this.map.disableGoogleBar(); }
													
			if (options.showTraffic)			{ this.addMapOverlay(new GTrafficOverlay(options.showIncidents), 'traffic'); }
											 
			if (options.showStreetview)			{ this.addMapOverlay(new GStreetviewOverlay(), 'street'); } 
											
			if (options.showPanorama)			{ this.map.overlays.panorama = this.updatePanoramaView(this.map.getCenter()); /*this.map.overlays.panorama = new GStreetviewPanorama(this.panoramaView, { latlng: this.map.getCenter() });
												  this.attachPanoramaEvents();
												  this.panoramaView.getParent().show(); */}						
											  
			if (options.disableDragging)		{ this.map.disableDragging(); }
			
			if (options.lockMap)				{ this.map.disableDragging();
												  this.map.hideControls();
												  this.map.disableGoogleBar(); }
												  
			if (options.hideCopyright)			{ this._handleCopyright(); }
			
			if (options.showWikipedia)			{ this.addMapOverlay( this.map.layers.wiki, 'wiki'); }
			
			if (options.showPanoramio)			{ this.addMapOverlay( this.map.layers.pano, 'pano'); }
			
			// create pointer (helper marker)
			this.pointer = this.createPointer();
		},	
		

		// init google bar
		setGoogleBarOptions: function() {
			
			// create searchResults container if it does not exist
			if (!$defined(this.searchResultsContainer)) {
				this.searchResultsContainer = new Element('div', { id: 'searchResults', styles: { display: 'none' } }).inject(document.body);	
			}
			
			var opts = {
				showOnLoad: 	true,
				linkTarget:						G_GOOGLEBAR_LINK_TARGET_BLANK, 	// default = G_GOOGLEBAR_LINK_TARGET_BLANK
				resultList:						this.searchResultsContainer,	// default = G_GOOGLEBAR_RESULT_LIST_INLINE; G_GOOGLEBAR_RESULT_LIST_SUPPRESS or Element
				suppressInitialResultSelection:	true,
				onIdleCallback: 				$empty,
				onSearchCompleteCallback: 		$empty,
				onGenerateMarkerHtmlCallback: 	$empty,	
				onMarkersSetCallback: 			function(args) {
					// remove all markers set by search
					$each(args, function(item, index) {
						this.map.removeOverlay(item.marker);	
					}.bind(this));
					
					// add first found marker to map
					var marker = args[0].marker;
					var latlng = marker.getLatLng();
					this.onClickMap(null, latlng);
				}.bind(this),
				suppressZoomToBounds:			false
			};
			
			return opts;
		},
		
				
		// set visible (enabled) sections based on icon type and map
		toggleVisibleSections: function() {	
			
			// enable/disable section AND fields
			function enable(sections, args, type) {
				$each(sections, function(el, index){
					if (args[index]) {
						var isEnabled = args[index][type];						
						(isEnabled) ? el.removeClass('disabled') : el.addClass('disabled');
						
						el.getElements('input, select, textarea').each(function(fld) {
							(isEnabled) ? fld.erase('disabled') : fld.set('disabled', true );								
						});
					}													   
				}.bind(this));
			}
			
			// set viewable "sections" per marker / static map
			var args = [
				// [ marker, labeledmarker, flat, static map ]
				[1,1,1,0],	// radIconType
				[1,0,1,0],	// txtIconWidth
				[0,0,1,0],	// txtIconHeight
				[0,0,0,1],	// radAltSize
				
				[1,1,1,0],	// txtIconPrimaryColor
				[0,0,0,1],	// txtIconAltColor
				[1,1,0,0],	// txtIconStrokeColor
				[1,0,0,0],	// txtIconCornerColor
				
				[0,1,1,0],	// chkAddLabel
				[0,0,1,0],	// txtLabelSize
				[0,1,1,0],	// txtLabelColor
				
				[0,1,0,0],	// chkAddStar
				[1,1,1,0],	// marker events
				
				[1,1,1,0],	// map setup
				[1,1,1,0],	// map controls
				
				[1,1,1,0],	// chkShowDirections
				[1,1,1,0],	// txtPolylineColor
				[0,0,0,1],	// txtPolylineAltColor
				[1,1,1,0],	// txtPolygonColor
				[1,1,1,0],	// chkShowWikipedia
				[1,1,1,0],	// chkShowPanoramio
				
				[0,0,0,1]	// map static
			];
			
			var isStaticMapView = (this.accordions[1].previous == 1) ? true : false;
			var iconIndex = isStaticMapView ? 99 : $('radIconType').get('inputValue');
			var type = 0;
			
			switch (parseInt(iconIndex)) {
				case 0:
					type = 0;
					break;
				case 1:
					type = 1;
					break;
				case 2: case 3: case 4:
					type = 2;
					break;
				case 99:
					type = 3;
					break;
				default:
					type = 0;
			}
			
			enable(this.togglableSections, args, type);	
					
			this._onChangeIconSize();	
		},
		
		// fn: dimMarker - add opacity to markers but current
		dimMarker: function(marker) {
			if (this.editMode) return false;
			if (!$chk(marker.elIcon)) return false;
			marker.elIcon.addClass('active');
			marker.elTips.fireEvent('mouseenter', marker.elTips);
			
			this.markers.each(function(item, index){
				if (!$chk(item.elIcon)) return false;						 
				if (item != marker) item.elIcon.setOpacity(0.25);
			})
		},
		
		
		// fn: dimMarkerReset - remove opacity from markers
		dimMarkerReset: function() {
			if (this.editMode) return false;
			
			this.markers.each(function(item, index){
				if (!$chk(item.elIcon)) return false;
				if (item.elIcon.hasClass('active')) { item.elIcon.removeClass('active'); item.elTips.fireEvent('mouseleave', item.elTips); }
				item.elIcon.setOpacity(1);	
			});
		},
		
		
		addMarkerToList: function(marker) {
			if (!$defined(marker)) return false;			
			
			if (this.markers.length == 0) {
				this.markerList.empty();
				new Element('ul', { id: 'markerList' }).inject(this.markerList); // create ul the first time
			}
			
			var ul = $('markerList');
			
			
			/*
			// init marker sortables
			if (!$chk(this.sortableMarkers)) {
				this.sortableMarkers = new Sortables(ul, {
					clone: false,
					opacity: 0.6,
					constrain: true,
					onStart: function(){ this.isSortingMarkers = true; dbug.log( 'onStart Sorting' ); }.bind(this),
					onSort: $empty,
					onComplete: function(el){						
						var aSort = this.sortableMarkers.serialize();
						dbug.log( 'onComplete Sorting: ' + this.sortableMarkers.serialize().length );
						//this.sortableMarkers.serialize(function(item, index){
							//aSort.push(item);										
						//})
						this.onSortMarkers(aSort);
					}.bind(this)
				});
			}*/
			
			var li = new Element('li', {	
				id: 'li_' + marker.get('id'),
				'events' : {
					'mouseenter' 	: function(ev) {
						ev.stop();
						this.dimMarker(marker);
					}.bind(this),
					'mouseleave'	: function(ev) {
						ev.stop();						
						this.dimMarkerReset();
					}.bind(this)
				}		
			});
			li.inject(ul);
			
			
			// add list item to sortables
			//this.sortableMarkers.addItems(li);
			
			// view marker
			li.grab(
				new Element('a', {
					//'text'		: marker.lat +', '+ marker.lng,
					'text'		: marker.title,
					'class'		: 'view',
					'title'		: 'View Marker',
					'href'		: '#',
					'events'	: {
						'click'	: function(ev) {
							ev.stop();
							this.onClickMarker(marker);
						}.bind(this),
						'mouseenter' : function(ev) {
							ev.stop()
						},
						'mouseleave' : function(ev) {
							ev.stop()
						}
					}
				})
			);
			
			// edit marker
			li.grab (
				new Element('a', {
					'class'		: 'btn edit',
					'title'		: 'Edit Marker',
					'href'		: '#',
					'events'	: {
						'click'	: function(ev) {
							ev.stop();
							this.editMode = false;
							this.onBeforeEditMarker(marker);						
						}.bind(this),
						'mouseenter' : function(ev) {
							ev.stop()
						},
						'mouseleave' : function(ev) {
							ev.stop()
						}
					}
				})
			);
			
			// remove marker
			li.grab (
				new Element('a', {
					'class' 	: 'btn remove',
					'title' 	: 'Remove Marker',
					'href'		: '#',
					'events'	: {
						'click'	: function(ev) {
							ev.stop();
							this.deleteMarker(marker);
						}.bind(this),
						'mouseenter' : function(ev) {
							ev.stop()
						},
						'mouseleave' : function(ev) {
							ev.stop()
						}
					}			
				})
			);
			
			return li;
		},
		
		
		// remove all marker overlays and delete all markers 
		deleteAllMarkers: function() {	
			if ( !confirm('Please confirm to delete all markers?') ) return false;
			
			// remove all markers	
			this.removeAllMarkers();
			this.markers.empty();	
			this.markerList.empty();
			this.directionList.empty();
			this.setPolyline();
			this.fireEvent('onChangeMap');
		},
		

		// remove all marker overlays
		removeAllMarkers: function() {			
			if (!this.markers) return false;
			this.markers.each(function(marker, index) {						
				this.map.removeOverlay(marker.ovl);
				marker.ovl = null;					
			}.bind(this));
		},
		
		
		// int to map type
		toMapType: function(type) {
			// 0 = G_NORMAL_MAP, 1= G_SATELLITE_MAP, 2= G_SATELLITE_3D_MAP (only Windows!), 4 = G_PHYSICAL_MAP, 3 = G_HYBRID_MAP
			switch(parseInt(type)) {
				case 0:
					return G_NORMAL_MAP;
					break;
				case 1:
					return G_SATELLITE_MAP;
					break;
				case 2:
					return G_SATELLITE_3D_MAP;
					break;
				case 3:
					return G_PHYSICAL_MAP;
					break;
				case 4:
					return G_HYBRID_MAP;
					break;
				default:
					return G_NORMAL_MAP;
			}
		},
		
		// map name to index
		mapNameToInt: function(name) {
			// 0 = G_NORMAL_MAP, 1= G_SATELLITE_MAP, 2= G_SATELLITE_3D_MAP (only Windows!), 3 = G_HYBRID_MAP, 4 = G_PHYSICAL_MAP
			switch(name.toLowerCase()) {
				case 'map':
					return 0;
					break;
				case 'satellite':
					return 1;
					break;
				case 'earth':
					return 2;
					break;
				case 'terrain':
					return 3;
					break;
				case 'hybrid':
					return 4;
					break;
				default:
					return 0;
			}
		},
		
		
		// int to travel mode
		toTravelMode: function(type) {
			// 0 = G_TRAVEL_MODE_DRIVING, 1 = G_TRAVEL_MODE_WALKING
			switch(parseFloat(type)) {
				case 0:
					return G_TRAVEL_MODE_DRIVING;
					break;
				case 1:
					return G_TRAVEL_MODE_WALKING;
					break;
				default:
					return G_TRAVEL_MODE_DRIVING; 	
			}
		},
		

		// int to zoom control
		toZoomControl: function(type) {
			switch (parseInt(type)) {
				case 0:
					return new GSmallZoomControl();
					break;
				case 1:
					return new GSmallMapControl();
					break;
				case 2:
					return new GLargeMapControl();
					break;
				default:
					return new GSmallMapControl();
			}
		},
		

		// int to icon type
		toIconType: function(type) {				
			switch (parseInt(type)) {
				case 0: // pin (no label)			
					return 'marker';
					break;
				case 1: // pin
					return 'labeledmarker';	
					break;
				case 2: case 3: case 4: // circle, square, rectangle
					return 'flat';	
					break;				
				default:
					return 'marker';					
			};
		},
		
		
		// int to icon shape
		toIconShape: function(type) {
			switch (parseInt(type)) {
				case 2: // circle			
					return 'circle';
					break;				
				case 3: case 4: // square, rectangle
					return 'roundrect';	
					break;				
				default:
					return 'circle';
			};
		},
		

		// fn: attachMarkerEvents
		attachMarkerEvents: function(overlay) {
			if (!$defined(overlay)) return false;
			
			// add marker event(s)
			GEvent.addListener(overlay, 'click', function(latlng) {
				if (overlay == this.pointer.ovl) return false;
				this.onClickMarker(this.getMarkerFromOverlay(overlay));		
			}.bind(this));	
			
			GEvent.addListener(overlay, 'mouseover', function(latlng) {
				if (overlay == this.pointer.ovl) return false;											  
				this.getMarkerFromOverlay(overlay).elList.addClass('active');
			}.bind(this));
			
			GEvent.addListener(overlay, 'mouseout', function(latlng) {
				if (overlay == this.pointer.ovl) return false;											 
				this.getMarkerFromOverlay(overlay).elList.removeClass('active');
			}.bind(this));	
			
			GEvent.addListener(overlay, 'mousedown', function(latlng) {
				this.onMousedownMarker(this.getMarkerFromOverlay(overlay), latlng);
			}.bind(this));

			GEvent.addListener(overlay, 'dragstart', function(latlng) {
				this.onDragStartMarker(this.getMarkerFromOverlay(overlay), latlng);
			}.bind(this))
			
			GEvent.addListener(overlay, 'drag', function(latlng) {	
				this.onDragMarker(this.getMarkerFromOverlay(overlay), latlng);
			}.bind(this))
			
			GEvent.addListener(overlay, 'dragend', function(latlng) {	
				this.onDragEndMarker(this.getMarkerFromOverlay(overlay), latlng);											
			}.bind(this))
		},
		
		
		// fn: getMarkerFromOverlay 
		getMarkerFromOverlay: function (overlay) {
			// pointer
			if (overlay == this.pointer.ovl) return this.pointer;
			
			// existing marker
			return this.markers.filter(function(item) {
				return item.ovl == overlay;							 
			})[0];	
		},

		
		// fn: iconCode
		iconCode: function(options) {
			var type = options.type;
			var keys = this.iconKeys[type];
			
			switch(type) {
				case 'marker':
				var fnName = 'createIcon';
				break;
				case 'labeledmarker':
				var fnName = 'createLabeledMarkerIcon';
				break;
				case 'flat':
				var fnName = 'createFlatIcon';
				break;
			}
			
			// create code
			var name = 'opts';
			var code = 'var ' + name + ' = {};' + "\n";
			var hash = options.filter(function(value, key) {
				return keys.indexOf(key) != -1;											 
			}).each(function(value, key){
				code 	+= name +'.'+ key + ' = ' + this.addQuotesToString(value) +';'+"\n";
			}.bind(this));
			
			code += 'var icon = MapIconMaker.' + fnName + '(' + name + ');' +"\n";
			
			var elCode = $('iconCode').getFirst();
			elCode.empty().set('text', code);
			return code;		
		},
		
		
		// fn: onClickMap
		onClickMap: function(overlay, latlng) {
			if (overlay) return false; // return if overlay is clicked
			if (!latlng) return false; // to do: disable forms?!
			
			// if infowindow - close it and do nothing
			if (this.markerInfo && this.markerInfo.visible) { this.hideInfoWindow(); return false; }
			
			var marker = null;
			var dat = this.getFormFields(this.frmMarkerSettings);
			latlng.y = latlng.y.round(6);
			latlng.x = latlng.x.round(6);
			
			if (this.editMode) {
				this.currentmarker.ovl.setLatLng(latlng);
				this.currentMarker.elList.getFirst().set('text', latlng.y +', '+ latlng.x);
				marker = this.currentMarker;			
				
			} else {
				// update pointer
				if (this.options.pointer) {
					this.updatePointer(latlng);
					this.pointer.ovl.show();
				}
				
				// show add/edit marker section
				if (this.accordions[0].previous != 1) this.accordions[0].display(1);
				if (this.accordions[2].previous != 1) this.accordions[2].display(1);
				
			}
			
			// update form fields
			$('txtLat').set ('value', latlng.y);
			$('txtLng').set ('value', latlng.x);
			$('txtZoom').set ('value', this.map.getZoom());
			$('txtTitle').set('value', latlng.y +', '+ latlng.x);
			
			// update geodata
			this.getAddressByLatLng(marker, latlng);
			
			this.updatePanoramaView(latlng);
		},
		
		
		// fn: onCloseInfoWindow
		onCloseInfoWindow: function() {
			this.centerMapToMarkers();
		},
		
		
		// onChange map
		onChangeMap: function() {
			dbug.log('updating static map');
			if (!this.options.syncOnChange) return false;
			var options = this.getIconOptions();
			
			this.updateStaticMap(options);
			this.iconCode(options);
			
			this.setDynamicMapCode();
		},
		
		// synchronize static map
		syncStaticMap: function() {
			var api		= this.options.api;			
			var key		= (window.location.hostname == 'localhost') ? api.key.local : api.key.remote;
			var center	= this.map.getCenter();
			var zoom 	= this.map.getZoom();
			//var dim = $('staticMapImg').getParent().getDimensions();	
			var dim		= { w: 456, h: 384 };
			var args 	= {
				center:		center.y +','+ center.x,
				zoom: 		zoom,
				maptype:	'roadmap',
				size: 		dim.w +'x'+ dim.h,
				key: 		key,
				format:		'gif'
			}	
			
			return args;
		},

		// update static map
		updateStaticMap: function(options) {		
			var markers = this.markers;
			
			var sizes	= ['normal', 'mid', 'small', 'tiny'];
			
			var mapSettings = this.getFormFields(this.frmMapSettings);
			//$each(mapSettings, function(item, index) { //console.log(index + ': ' + item) });
			// icon
			//dbug.log('size: ' + $('radAltSize').get('inputValue'));
			//var iconSize	= (options.alternateSize == 0) ? '' : sizes[options.alternateSize];
			var iconSize	= (options.alternateSize == 0) ? '' : sizes[options.alternateSize];
			
			var iconColor	= this.getNamedColor(options.alternateColor)[1].toLowerCase();
			
			// polyline			
			var polyShow 	= (mapSettings.chkShowPolyline && mapSettings.radPolyType == 0) ? true : false; // show only if poly checked AND type eq. line
			var polyColor 	= mapSettings.txtPolylineAltColor.replace('#', '');
			var polyAlpha	= parseInt(mapSettings.selPolyAlpha/100*255).toString(16);			
			var polyWeight 	= mapSettings.selPolyWeight;
			
			var args = new Hash(this.syncStaticMap());
			
			args.format 	= mapSettings.radStaticMapFormat;
			args.maptype 	= mapSettings.radStaticMapType;
			
			// create markers
			if (markers && markers.length > 0) {
				// center to markers
				args.erase('center');
				args.erase('zoom');
			
				if (polyShow) {
					args.path	 	= 'rgba:0x' + polyColor+polyAlpha +',weight:'+polyWeight + '|';
				}
				
				// markers.push([marker, {label: iconOptions.label }]);
				
				var strDelim = '|';
				args.markers = ''; 			
				$each(markers, function(item, index) {
					pos = item.ovl.getLatLng();
				
				
					if ((markers.length -1) <= index) strDelim = ''; // last item reached - remove delimiter				
					label = (iconSize == 'small' || iconSize == 'tiny') ? '' : item.label.substring(0,1).toLowerCase();				
					args.markers += pos.y +','+pos.x +','  +iconSize + iconColor +label + strDelim;
					
					// add string for polyline	
					if (polyShow) args.path += pos.y +',' + pos.x + strDelim;				
				});
			}
				
			// create map url
			var dim = args.size.split('x');
			var el = $('staticMapImg');
			var url = 'http://maps.google.com/staticmap';
			
			var src = url + '?' + decodeURIComponent(args.toQueryString()); 			
			el.set({ src: src, width: dim[0], height: dim[1] });
			
			// update lightbox link
			args.size = Math.min(640, mapSettings.txtStaticMapWidth) +'x'+Math.min(640, mapSettings.txtStaticMapHeight);
			src = url + '?' + decodeURIComponent(args.toQueryString()); 
			el.getParent().set('href', src);	
			
			
			// set code
			var optionsName = 'options';
			var strCode = 'var '+optionsName+' = {};' + "\n";
			$each(args, function(value, key) {
				strCode += optionsName+'.'+ key + ' = \'' + value + '\';' + "\n";   
			});
			
			strCode += "\n";
			strCode += src +"\n";
			$('staticMapCode').getFirst().set('text', strCode);	
		},
		
		// set events // todo: attach events after map has been initialized!!! (current timing issue)
		attachEvents: function() {
			
			// MARKER SETTINGS
			this.attachFormEvents(this.frmMarkerSettings, this._handleMarkerSettingsChange.bind(this));
			
			// MAP SETTINGS			
			this.attachFormEvents(this.frmMapSettings, this._handleMapSettingsChange.bind(this));
			
			// MENU BUTTONS
			$$('li.btn').addEvent('click', function(ev) { ev.stop(); this._handleMenuButton(ev) }.bind(this));
			
			// close panarama view
			$('btnClosePanoramaView').addEvent('click', function(ev) {
				ev.stop();
				this.map.overlays.erase('panorama');
				this.panoramaView.empty().getParent().hide();				
				$('chkShowPanorama').set('checked', false);
			}.bind(this));
			
			
			$('btnResizePanoramaView').addEvent('click', function(ev) {
				this.togglePanoramaSize(ev);
			}.bind(this));
			
			
			// add/edit marker 
			$('submitAddMarkerForm').addEvent('click', function(ev) {
				ev.stop();
				this.editMode ? this.onClickEditMarker() : this.onClickAddMarker();
			}.bind(this));
			
			// cancel add/edit marker
			$('cancelAddMarkerForm').addEvent('click', function(ev) {
				ev.stop();
				this.editMode ? this.onClickCancelEditMarker() : this.onClickCancelAddMarker();	
			}.bind(this));
			
			// add xml/kml file
			$('submitLoadXMLForm').addEvent('click', function(ev) {
				ev.stop();
				var fName = $('txtFile').get('value');
				if (!fName) return false;
				this.onClickAddXMLFile(fName);				
				//this.editMode ? this.onClickCancelEditMarker() : this.onClickCancelAddMarker();	
			}.bind(this));
			
			
			// cancel xml/kml file
			$('cancelLoadXMLForm').addEvent('click', function(ev) {
				ev.stop();
				$('txtFile').set('value', '');
				//this.editMode ? this.onClickCancelEditMarker() : this.onClickCancelAddMarker();	
			}.bind(this));
			
			
			
			// geocoder buttons
			$('submitGeoCoderForm').addEvent('click', function(ev){
				ev.stop();
				this.getLatLngByAddress();				
			}.bind(this));
			
			$('cancelGeoCoderForm').addEvent('click', function(ev) {
				ev.stop();												 
				this.geocoderForm.reset();
			}.bind(this));
			
			// browse pictures
			$('browseFiles').addEvent('click', function(ev) {
				ev.stop();
				this.browsePictures();
			}.bind(this));
			
			// browse kml/xml files
			$('browseFile').addEvent('click', function(ev) {
				ev.stop();
				this.browseFiles();
			}.bind(this));
		},
		
		
		// fn: attachPanoramaEvents 
		attachPanoramaEvents: function() {
			if (!this.map.overlays.has('panorama')) return false;
			var panorama = this.map.overlays.panorama;
			
			GEvent.addListener(panorama, 'error', function(error) {
				//console.log('panorama error');
				var html = '';
				if (error == 603) {
					html = '<p class="error">Error: flash doesn\'t appear to be supported by your browser!</p>';
		
				} else if (error == 600) {
					html = '<p class="error">Error: no street view available!</p>';	
				}
				
				this.panoramaView.empty().set('html', html);
				return false;				
			}.bind(this));	
			
			GEvent.addListener(panorama, 'initialized', function(location) {
				//console.log('panorama initialized');
				// fired when the panorama is initialized after moving to a new location
				return false;		  
			}.bind(this));
			
			GEvent.addListener(panorama, 'yawchanged', function(location) {
				// fired when the yaw is changed in the flash viewer
				//console.log('panorama yaw changed');
				return false;															  
			}.bind(this));
			
			GEvent.addListener(panorama, 'pitchchanged', function(location) {
				// fired when the pitch displayed in the flash viewer is changed
				//console.log('panorama pit changed');
				return false;																		  
			}.bind(this));
		},
		
		
		// toggle panorama size
		togglePanoramaSize: function(ev) {
			ev.stop();
			
			// morph panorama
			if (!this.morphPanorama) {
				// set effect
				this.morphPanorama = new Fx.Morph(this.panoramaView, {
					duration: 500,
					start: function() {
						if (!$defined(this.panoramaView.getFirst())) return false;
						this.panoramaView.getFirst().hide();
					}, 				
					onComplete: function() {						
						if (!$defined(this.panoramaView.getFirst())) return false;
						this.panoramaView.getFirst().setStyle('height', this.panoramaView.getStyle('height').toInt()).show();
					}.bind(this)	
				});
				
				// store initial values
				var height = {
					min: this.panoramaView.getStyle('height').toInt(),
					max: this.panoramaView.getParent().getParent().getStyle('height').toInt()
				}
				this.panoramaView.store('height', height);
			}
			
			// morph height
			var h = this.panoramaView.retrieve('height');
			var isFull = (this.panoramaView.getStyle('height').toInt() == h.max) ? true : false;
			var range = isFull ? [h.max, h.min] : [h.min, h.max];
			this.morphPanorama.start({ height: range });
		},
		
		
		// fn: updatePanoramaView
		updatePanoramaView: function(latlng) {
			if (!$('chkShowPanorama').checked) {
				this.panoramaView.empty().getParent().hide();
				return false;
			}
			
			if (!$defined(latlng)) latlng = this.map.getCenter();
			//if (this.map.overlays.has('panorama')) this.map.overlays.erase('panorama');
			
			if (!this.map.overlays.has('panorama')) {
				this.map.overlays.panorama = new GStreetviewPanorama(this.panoramaView, { latlng: latlng });
				this.attachPanoramaEvents();
			}
			this.panoramaView.getParent().show();
			
			
			this.map.overlays.panorama.setLocationAndPOV(latlng);
			
			return this.map.overlays.panorama;
		},
		
		
		// fn: attachFormEvents - add events to form fields
		attachFormEvents: function(form, fn) {
			var els = form.getElements('input, select, textarea');
			$each(els, function(el, index) { 				
				switch(el.get('tag')) {
					case 'select':						
							return el.addEvent('change', function(ev) { fn(ev) });
					case 'input':
						if (['radio','checkbox'].contains(el.get('type'))) {
							return el.addEvent('click', function(ev) { fn(ev) });
						}						
					case 'input': case 'textarea':
						return el.addEvent('change', function(ev) { fn(ev) });
					default:
						return false;
				 }			
			})
		},
		
		
		// handle change of marker settings		
		_handleMarkerSettingsChange: function(ev) {
			dbug.log('fn: _handleMarkerSettingsChange');
			if ($type(ev) != 'event') { dbug.log('invalid marker settings event'); return false; }
			
			var el 		= ev.target;
			var value	= el.get('inputValue');
			var name	= el.get('name');
			
			switch(name) {
				case 'radIconType':
					this.toggleVisibleSections(ev);
					break;
				case 'txtIconSize':
					this._onChangeIconSize(ev);
					break;
				case 'chkShowTip':
					(value) ? this.enableTipMarker() : this.disableTipMarker();
					return false;
					break;
				case 'chkShowImage':
					this.toggleMarkerImage(value);
					return false;
					break;
				case 'chkAllowDrag':
					(value) ? this.enableDragMarker() : this.disableDragMarker();
					return false;
					break;
			}
			
			this.replaceMarker();
			this.setPolyline();
			this.fireEvent('onChangeMap');
		},
		
		
		// handle change of map settings
		_handleMapSettingsChange: function(ev) {			
			if ($type(ev) != 'event') return false;			
			var el		= ev.target;
			var value	= el.get('inputValue');
			var name	= el.get('name');
			
			switch(name) {
				// map settings
				case 'radMapType':
					this.mapTypeHasChanged = true;	
					var mapType = this.toMapType(value);
					if (this.map.controls.has('overview')) { // hide overview if satellite 3D
						this.map.controls.overview.setMapType(mapType);
						(value == 2) ? this.map.controls.overview.hide() : this.map.controls.overview.show();
					}
					
					return this.map.setMapType(mapType);
					break;
					
				case 'chkShowMapType': case 'chkUseShortNames':
					this.removeMapControl('type');
					if (!$('chkShowMapType').checked) return false;
					var useShortNames = $('chkUseShortNames').get('inputValue');						
					this.addMapControl(new GMapTypeControl(useShortNames),'type');
					break;
					
				case 'chkLockMap':
					el.checked ? this.map.hideControls() 		: this.map.showControls();
					el.checked ? this.map.disableGoogleBar()	: this.map.enableGoogleBar();
					el.checked ? this.map.disableDragging()	: this.map.enableDragging();					
					break;
					
				case 'chkDisableDragging':
					(el.checked) ? this.map.disableDragging()	: this.map.enableDragging();
					break;
					
				case 'chkHideCopyright':
					this._handleCopyright();
					break;
				
				case 'chkHideControls':					
					el.checked ? this.map.hideControls() 		: this.map.showControls();
					el.checked ? this.map.disableGoogleBar()	: this.map.enableGoogleBar(); // to do: enable google bar only if search checked							
					this._handleCopyright();
					break;
					
				case 'chkShowOverview':						
					this.removeMapControl('overview');			
					if (!el.checked) return false;					
					var ctrl = this.addMapControl(new GOverviewMapControl(new GSize(150, 150)), 'overview');
					ctrl.setMapType(this.map.getCurrentMapType());
					break;
					
				case 'chkShowZoom': case 'radZoomType':
					this.removeMapControl('zoom');
					if (!$('chkShowZoom').checked) return false;					
					this.addMapControl(this.toZoomControl($('radZoomType').get('inputValue')), 'zoom');					
					break;
					
				case 'chkShowSearch':
					el.checked ? this.map.enableGoogleBar() : this.map.disableGoogleBar();					
					break;
					
				case 'chkShowScale':					
					this.removeMapControl('scale');
					if (!el.checked) return false;
					this.addMapControl(new GScaleControl(), 'scale');						
					break;
					
				case 'chkShowPolyline': case 'chkShowDirections': case 'radPolyType': case 'radTravelMode': case 'chkAvoidHighway': case 'selPolyWeight': case 'selPolyAlpha':
					this.removeMapOverlay('polyline');
					this.tabs[5].disable(1); // disable directions
					//this.fireEvent('onChangeMap');	
					if (!$('chkShowPolyline').checked && !$('chkShowDirections').checked) { this.fireEvent('onChangeMap'); return false; }
					this.setPolyline(ev);					
					break;
					
				case 'chkShowWikipedia':					
					this.removeMapOverlay('wiki');
					if (!el.checked) return false;
					this.addMapOverlay(this.map.layers.wiki, 'wiki');
					break;
					
				case 'chkShowPanoramio':
					this.removeMapOverlay('pano');
					if (!el.checked) return false;
					this.addMapOverlay(this.map.layers.pano, 'pano');
					break;
				
				case 'chkShowTraffic': case 'chkShowTrafficIncidents':
					this.removeMapOverlay('traffic');
					if (!$('chkShowTraffic').checked) return false;
					var showIncidents = $('chkShowTrafficIncidents').get('inputValue');						
					this.addMapOverlay(new GTrafficOverlay(showIncidents), 'traffic');				
					break;
					
				case 'chkShowStreetview':					
					this.removeMapOverlay('street');					
					if (!el.checked) return false;
					this.addMapOverlay(new GStreetviewOverlay(), 'street');
					break;
					
				case 'chkShowPanorama':
					//this.map.overlays.erase('panorama');
					//this.panoramaView.empty().getParent().hide();
					//if (!el.checked) return false;
					this.updatePanoramaView(this.map.getCenter());
					break;	
					
				default:
					dbug.log('fn _mapSettingsChange: static map settings changed - default action');
					return false;
			}

			this.fireEvent('onChangeMap');	
		},
		
		// onChange iconSize
		_onChangeIconSize: function(ev) { 
			var el 			= ($type(ev) == 'event') ? ev.target : $('txtIconWidth');
			var size 	  	= el.get('inputValue');
			var type     	= $('radIconType').get('inputValue');
			var elWidth  	= $('txtIconWidth');
			var elHeight	= $('txtIconHeight');
			
			if (type == 4) { // rounded rectangle - no proportions
				elHeight.erase('disabled').removeClass('disabled').getNext().removeClass('disabled');
				size = Math.min(elWidth.get('value'), elHeight.get('value'));
			} else {
				elWidth.set('value', size);							
				elHeight.set({ disabled: true, value: size }).addClass('disabled').getNext().addClass('disabled');
			}
			
			if ($defined(ev)) this.updateAlternateSize(size); // only on real change of icon size
			this.updateLabelSize(size);			
		},


		// fn: _onChangeAlternateSize
		_onChangeAlternateSize: function() {			
			this.fireEvent('onChangeMap');	
		},
		
		
		// fn: updateLabelSize
		updateLabelSize: function(size) {
			var el		= $('txtLabelSize');
			var factor	= 0.5;
			var min 	= 7;
			el.set('inputValue', Math.max(min, (size*factor).round()));
		},
		
		
		// fn: updateAlternateSize
		updateAlternateSize: function(size) {	
			var match = function(sizes, val) {
				  var smallest, closest;
				  sizes.getValues().each(function(v){
						var diff = Math.abs(val-v);
						if(!$chk(smallest) || smallest > diff) {
							  smallest = diff;
							  closest = v;
						}
				  });
				  return sizes.keyOf(closest);
			};
	
			var sizes	= $H({ normal: 32, mid: 28, small: 20, tiny: 12 });
			var keys 	= $H(sizes.getKeys());
			var size 	= keys.keyOf(match(sizes, size));
						
			$('radAltSize').set('inputValue', size);
		},
		
		
		// fn: _onChangeColor
		_onChangeColor: function(el, color) {	
			var id = el.get('id');
			
			switch (id) {
				case 'txtIconPrimaryColor':
					this.updateAlternateColor(color);
					this.updateLabelColor(color);
					break;
				
				case 'txtIconAltColor': case 'txtPolylineAltColor':
					return this.onChangeMap();	
					break;
				
				case 'txtPolylineColor': case 'txtPolygonColor':
					return this.setPolyline();
					break;

				default:
					//return false;
					break;
			}
			this.replaceMarker();
		},
		
		
		// fn: updateAlternateColor - main color for static map
		updateAlternateColor: function(color) {			
			if (!$defined(color)) color = $('txtIconPrimaryColor').get('inputValue');			
				
			// update color for static map
			var el = $('txtIconAltColor');
			var altColor = this.getNamedColor(color)[0];			
			el.set('value', altColor);
			el.getPrevious().setStyle('background-color', altColor);
			
			return altColor;
		},
		
		
		// fn: updateLabelColor - invert color
		updateLabelColor: function(color) {
			var invertedColor = new Color(color).invert().rgbToHex();
			var colors = [
				['000000', 'Black'],			
				['ffffff', 'White']
			];
			var namedColor = new Color(invertedColor).match(colors)[0];	
				
			el = $('txtLabelColor');
			el.set('value', namedColor);
			el.getPrevious().setStyle('background-color', namedColor);
			
			return namedColor;
		},
		
		
		// fn: _onChangeLabelType
		_onChangeLabelType: function(ev) {			
			var el 			= ev.target;
			var type 		= el.get('value'); 	
			
			var elLabelText = $('labelText');
			
			if (type !=1) { // custom label
				elLabelText.set({ disabled: true, value: '' }).addClass('disabled');
				elLabelText.getParent().getPrevious().getFirst().addClass('disabled');
			} else {
				elLabelText.erase('disabled').removeClass('disabled');
				elLabelText.getParent().getPrevious().getFirst().removeClass('disabled');
			}
			
			this.changeAllIcons();
		},
		
		
		// fn: _handleMenuButton
		_handleMenuButton: function(ev) {
			if ($type(ev) != 'event') { dbug.log('Error: _handleMenuButton event'); return false; }
			
			var el = ev.target.getParent();
			var id = el.id;	
			
			switch (id) {
				case 'btnCenterToMarkers':
					this.centerMapToMarkers();
					break;
					
				case 'btnStaticDownload':
					var form	= $('frmDownload');
					var src 	= $('staticMapImg').getParent().get('href');
					var fmt 	= Browser.getQueryStringValue('format', src);
					$('fileName').set('value', src);
					$('fileType').set('value', fmt);
					return form.submit();
					break;
					
				case 'btnStaticEnlarge':
					if (!this.lightbox) { this.lightbox = new Lightbox(); this.lightbox.init(); } 
					this.lightbox.show($('staticMapImg').getParent().get('href'), 'Google Map');
					break;
					
				case 'btnStaticRefresh':
					this.fireEvent('onChangeMap');	
					break;
					
				case 'btnMarkerListDeleteAll':
					this.deleteAllMarkers();
					break;
					
				default:
					return false;
					break;
				
			}
		},
		
		
		// fn: getFormFields - requires cnet plugin
		getFormFields: function(el) {		
			var form 	= $(el);
			var hash	= new Hash();
			var els 	= form.getElements('input, select, textarea');
			$each(els, function(el) {				
				if (el.get('id')) hash[el.get('id')] = el.get('inputValue');				
			});
			return hash;		
		},
		
		
		// fn: getNamedColor
		getNamedColor: function(color) {
			var colors = [
				['000000', 'Black'],
				['8B4513', 'Brown'],
				['008000', 'Green'],
				['800080', 'Purple'],
				['ffff00', 'Yellow'],
				['0000FF', 'Blue'],
				['808080', 'Gray'],
				['ffa500', 'Orange'],
				['ff0000', 'Red'],
				['ffffff', 'White']
			];
			
			return new Color(color).match(colors);		
		},
		
		
		// fn: _handleCopyright - show/hide Google logo and terms	
		_handleCopyright: function() {
			var el 		= $('chkHideCopyright');
			var spanEls	= this.mapCanvas.getElements('div span');	  				
			$each(spanEls, function(item) { 
				parent = item.getParent();
				el.checked ? parent.hide() : parent.show();
			})				
		},
		
		
		// fn: addMapControl
		addMapControl: function(control, key) {			
			this.map.controls[key] = control;
			this.map.addControl(control);
			return control;
		},
		
		
		// fn: removeMapControl
		removeMapControl: function(name) {
			if (!name) return false;
			var ctrls = this.map.controls;
			if (ctrls.has(name)) { this.map.removeControl(ctrls[name]); ctrls.erase(name); }
			return ctrls;			
		},
		
		
		// fn: addMapOverlay
		addMapOverlay: function(overlay, key) {
			this.map.overlays[key] = overlay;
			this.map.addOverlay(overlay);
			return overlay;
		},
		

		// fn: removeMapOverlay
		removeMapOverlay: function(name) {
			if (!name) return false;
			var olays = this.map.overlays;
			if (olays.has(name)) { this.map.removeOverlay(olays[name]); olays.erase(name); }
			return olays;		
		},
		
		
		// fn: centerMapToMarkers
		centerMapToMarkers: function() {
			if (this.markers.length == 0) return false; // no markers to center
			var bounds = new GLatLngBounds();
			$each(this.markers, function(item){ bounds.extend(item.ovl.getLatLng())})
			this.map.setCenter(bounds.getCenter(), Math.min(this.map.getZoom(), this.map.getBoundsZoomLevel(bounds))); 
		},
		
		
		// handle polyline check fields as radio buttons
		handlePolyline: function(ev) {			
			var el = $chk(ev) ? ev.target : $('chkShowPolyline');
			var elPolyline = $('chkShowPolyline');
			var elDirections = $('chkShowDirections');
			
			if ( el == elPolyline || el == elDirections && el.checked) {
				if (el == elPolyline) {
					elDirections.erase('checked');
				} else if (el == elDirections) {
					elPolyline.erase('checked');
				}
			}				
			return false;
		},
		
		
		// update polyline
		setPolyline: function(ev) {
			if ($chk(ev)) this.handlePolyline(ev);
			this.removeMapOverlay('polyline');			
			if (this.markers.length < 2) return false;
			
			var showPolyline 	= $('chkShowPolyline').checked; 
			var showDirections	= $('chkShowDirections').checked;

			if (!showPolyline && !showDirections) return false; // none is checked
			
			var args = {
				line:		$('txtPolylineColor').get('inputValue'),
				fill:		$('txtPolygonColor').get('inputValue'),
				alpha:		$('selPolyAlpha').get('inputValue')/100,
				weight:		$('selPolyWeight').get('inputValue')*1,
				type:		$('radPolyType').get('inputValue')
			}
			
			var latlngs 	= this.markers.map(function(item, index) { return item.ovl.getLatLng(); });
			
			if (showPolyline) {	
				this.updatePolyline(latlngs, args);				
			} else if (showDirections) {	
				this.tabs[5].enable(1);
				this.updateDirections(latlngs, args);
			}
		},
		
		// update polyline
		updatePolyline: function(latlngs, args) {			
			var a = args;
			
			var options = {
				clickable:	true,
				geodesic: 	true
			};
			
			var overlay = (a.type == 0) ? new GPolyline(latlngs, a.line, a.weight, a.alpha, options) : new GPolygon(latlngs, a.line, a.weight, a.alpha, a.fill, a.alpha, options);
			this.addMapOverlay(overlay, 'polyline');						
		},
		
		
		// fn: updateDirections
		updateDirections: function(latlngs, args) {
			if (latlngs.length > 25) { dbug.log('Error: unable to process directions - max. number of waypoints reached.'); return false; }
			
			var a = args;
			a.mode 	= this.toTravelMode($('radTravelMode').get('inputValue'));
			a.hwy 	= $('chkAvoidHighways').checked;
			
			var options = {
				locale: 			'en_US',
				travelMode:			a.mode,
				avoidHighways:		a.hwy,
				getPolyline: 		true,
				getSteps: 			true,
				preserveViewport:	true
			}
			
			if (!this.map.overlays.has('directions')) { // not yet added				
				this.map.overlays.directions = new GDirections(null, null);
				
				GEvent.addListener(this.map.overlays.directions, 'load', function() {		
					var overlay		= this.map.overlays.directions.getPolyline();
					overlay.color   = a.line; 
					overlay.weight  = a.weight;
					overlay.opacity	= a.alpha;
					this.addMapOverlay(overlay, 'polyline');
					
					this.directionList.empty();
					this.buildDirections().inject(this.directionList);
					
					
				}.bind(this))
			}
			
			this.map.overlays.directions.loadFromWaypoints(latlngs, options);
		},
		
		
		// fn: buildDirections
		buildDirections: function() {
			d = this.map.overlays.directions;			
			var container = new Element('div', { 'class': 'directions' });
			var self = this;
			
			// build point (section)
			function setPoint(address, index) {
				var ul = new Element('ul', { 'class': 'points' });
				ul.inject(container);
			
				var li = new Element('li', { 'class': 'point', 'html': address });
				new Element('img', {
					'src': self.markers[index].ovl.getIcon().image,
					'styles': {
						'cursor':'pointer'
					},
					'events': {
						'click' : function() {
							self.markers[index].ovl.showMapBlowup();
						},
						'mouseenter' : function() {
							self.dimMarker(self.markers[index]);	
						},
						'mouseleave': function() {
							self.dimMarkerReset();
						}
					}
				}).inject(li, 'top');
				li.inject(ul);
			
				return ul;
			};
			
			$each(this.markers, function(item, index){
				if (index > 0) {
					pindex	= (index - 1);
					route 	= d.getRoute(pindex);
					geocode	= route.getStartGeocode();
					point	= route.getStep(0).getLatLng();
					ul 		= setPoint(geocode.address, pindex);
					
					new Element('li', { 'class': 'right time', 'html':  route.getDistance().html + ' (about ' + route.getDuration().html + ')' }).inject(ul);
					
					var li = new Element('li');
					li.inject(ul);
					var ol = new Element('ol', { 'class' : 'steps' });
					ol.inject(li);
					
					// process steps
					for (var j = 0; j < route.getNumSteps(); j++) {
						step = route.getStep(j);
						new Element('li', { 'html' : step.getDescriptionHtml() +', ' + '<span class="distance">' + step.getDistance().html + '</span>' }).inject(ol);
					}
					
					// finalize directions (add last point)
					if (index + 1 == this.markers.length) setPoint(geocode.address, index);
				}										 
			}.bind(this))
			return container;
		},


		// get distance
		getDistance: function () {
			if (this.markers.length < 2) return false;	// at least 2 markers are needed
			var d = this.map.overlays.directions;
			var isCheckedDirections = $('chkShowDirections').checked;	//true = distance by road; false = distance as crow flies	
			
			var route, timeToPrev, timeToFirst, distToPrev, distToFirst, latlngOfFirst, latlngOfPrev;
			route			= null;
			timeToPrev		= 0; // seconds to previous
			timeToFirst		= 0; // seconds to first
			distToPrev		= 0; // meters to previous
			distToFirst		= 0; // meters to first
			
			latlngOfFirst	= this.markers[0].ovl.getLatLng();
			
			this.markers.each(function(item, index) {
				if (index > 0) {
					pindex	= (index - 1);
					if (isCheckedDirections) {
						route 		 = d.getRoute(pindex);				
						timeToPrev 	 = rte.getDuration().seconds 		
						timeToFirst	+= timeToPrev;
						distToPrev 	 = route.getDistance().meters;
						distToFirst	+= distToFirst + distToPrev; 									
					} else {
						latlng		 = item.ovl.getLatLng();
						latlngOfPrev = this.markers[pindex].ovl.getLatLng();
						distToPrev 	 = latlng.distanceFrom(latlngOfPrev);
						distToFirst	 = latlng.distanceFrom(latlngOfFirst);
					}
				}
				
				// set item data
				item.timeToPrev		= timeToPrev; 	// seconds to previous
				item.timeToFirst	= timeToFirst; 	// seconds to first
				item.distToPrev		= distToPrev;	// meters to previous
				item.distToFirst	= distToFirst; 	// meters to first
			}.bind(this));
		},
		
		
		// fn: onClickMarkers
		onClickMarker: function(marker) {
			marker.elTips.fireEvent('mouseleave'); // hide marker tip
			
			// create info window if not setup yet
			if (!$chk(this.markerInfo)) {
				this.markerInfo = new StickyWin.Fx({
					width: 			300,
					/*height: 		150,*/
					className: 		'markerInfo',
					fade: 			true,
					fadeDuration:	500,
					draggable: 		false,
					content: 		'',
					position: 		'upperLeft',
					relativeTo: 	this.mapCanvas,
					offset: 		{ x: 50, y: 50 },
					useIframeShim:	false,
					//timeout:		3000,
					onDisplay: 		$empty,
					onClose:		function() { this.map.returnToSavedPosition(); }.bind(this),
					showNow: 		false
				});
				
				// set default (empty) content
				var div = new Element('div', {
					id: 		'markerInfoContainer',
					'class':	'tabContainer'
				})
				
				// set close button
				new Element('div', { 'class': 'closeInfo right', title: 'Click to close', events: { click: function(ev){ this.hideInfoWindow()}.bind(this)} }).inject(div);
	
				
				// set html for tabbed content
				var ul = new Element('ul', {
					'class': 'tabs clearfix'
				}).inject(div)
				
				new Element('li', { 'class': 'on', html: '<span>Info</span>' }).inject(ul);
				new Element('li', { html: '<span>Close-up</span>' }).inject(ul);
				new Element('li', { html: '<span>Location</span>' }).inject(ul);
				new Element('li', { html: '<span>Text</span>' }).inject(ul);
				new Element('li', { html: '<span>Image</span>' }).inject(ul);
				new Element('li', { html: '<span>Options</span>' }).inject(ul);
				
				
				var panels = new Element('div', { 'class': 'panels' }).inject(div);
				new Element('div', { 'class': 'panel', text: '<!-- info        -->' }).inject(panels);
				new Element('div', { 'class': 'panel', text: '<!-- close-up    -->' }).inject(panels);
				new Element('div', { 'class': 'panel', text: '<!-- location    -->' }).inject(panels);
				new Element('div', { 'class': 'panel', text: '<!-- text        -->' }).inject(panels);
				new Element('div', { 'class': 'panel', text: '<!-- image       -->' }).inject(panels);
				new Element('div', { 'class': 'panel', text: '<!-- options     -->' }).inject(panels);

				this.markerInfo.setContent(div);
				
				
				// init tabs in infoWindow
				var el = div;
				this.tabs.push(
					new TabSwapper({
						tabs			: el.getElements('ul.tabs li'),
						sections		: el.getElements('div.panel'),
						selectedClass	: 'on',
						deselectedClass	: 'off',
						disabledClass	: 'disabled',			
						onActive		: $empty, // (index, panel, tab)
						onActiveFx		: $empty, // (index, panel, tab)	
						onBackground	: $empty  // (index, panel, tab)
					})
				);	
			}
			
			
			// disable not needed tabs based on marker settings
			marker.showImage ? this.tabs[6].enable(4) : this.tabs[6].disable(4);
			
			
			this.chains.moveMap.chain(
				function() {
					this.setInfoWindowContent(marker);
					this.showInfoWindow();
				}.bind(this)
			);
			
			this.map.panTo(this.moveMapToNorthWest(marker));
		},
		
		
		// fn: showInfoWindow
		showInfoWindow: function() {
			if (!$chk(this.markerInfo)) return false;
			
			if (!this.markerInfo.visible) this.markerInfo.show();
			return this.markerInfo;
		},
		
		
		// fn: hideInfoWindow
		hideInfoWindow: function() {
			if (!$chk(this.markerInfo)) return false;
			if (this.markerInfo.visible) this.markerInfo.hide();
			
			// return to saved map position
			//this.map.returnToSavedPosition.delay(500, this.map);
			return this.markerInfo;	
		},
		
		
		// fn: moveMapToNorthWest - move map and marker to upper left position
		moveMapToNorthWest: function(marker) {
			if (!$defined(marker)) return false;
			this.map.savePosition();	// save current map position
			
			var offset		= this.markerInfo.options.offset;
			var proj		= this.map.getCurrentMapType().getProjection();
			var px 			= proj.fromLatLngToPixel(marker.ovl.getLatLng(),this.map.getZoom());
			var sePx 		= new GPoint(px.x - offset.x + Math.floor(this.mapCanvas.clientWidth / 2),px.y - offset.y + Math.floor(this.mapCanvas.clientHeight / 2));
			var seLatLng	= proj.fromPixelToLatLng(sePx,this.map.getZoom());
			
			return seLatLng;
		},
		
		
		setInfoWindowContent: function(marker) {
			
			var tabgrp	= this.tabs[6];
			var latlng	= marker.ovl.getLatLng();
			
			var strDistance = $('chkShowDirections').checked ? 'Distance:' : 'Distance (linear):';
			

			this.getDistance();
			
			// info panel
			var html = '';
			html += '<img class="right" src="' + marker.ovl.getIcon().image + '">';
			html += '<dl class="info">'; 
			html += '  <dt><label>Title:</label>'			+ marker.title 						+ '</dt>';
			html += '  <dt><label>Label:</label>'			+ marker.label 						+ '</dt>';
			html += '  <dt><label>Altitude:</label>'		+ marker.alt 						+ '</dt>';
			html += '  <dt><label>Latitude:</label>'		+ this.convert(latlng.y, 'lat') 	+ '</dt>';
			html += '  <dt><label>&nbsp;</label>'			+ latlng.y 							+ '</dt>';
			html += '  <dt><label>Longitude:</label>'		+ this.convert(latlng.x, 'lng') 	+ '</dt>';
			
			html += '  <dt><label>&nbsp;</label>'			+ latlng.x 							+ '</dt>';
			html +='  <dt><label>'+strDistance+'</label>'	+ '&nbsp;' 							+ '</dt>';
			html +='  <dt><label>...to previous:</label>'	+ this.convert(marker.distToPrev,'km', 1) + ' km' + ' (<em>'+ this.convert(marker.distToPrev, 'mi', 1) + ' mi' + '</em>)' + '</dt>';
			html +='  <dt><label>...to start:</label>'		+ this.convert(marker.distToStart,'km', 1) + ' km' + ' (<em>'+ this.convert(marker.distToStart, 'mi', 1) + ' mi' + '</em>)' + '</dt>';
			html +='  <dt><label>Duration:</label>'			+ '&nbsp;' 							+ '</dt>';
			html +='  <dt><label>...to previous:</label>'	+ this.convert(marker.timeToPrev, 'HMS') 	+ '</dt>';
			html +='  <dt><label>...to start:</label>'		+ this.convert(marker.timeToStart, 'HMS') 	+ '</dt>';
			
			html += '</dl>';
			
			tabgrp.tabs[0].retrieve('section').empty().set('html', html);


			// close-up
			this.setCloseUpMap(marker);
			//tabgrp.tabs[1].retrieve('section').empty().set('html', 'close-up');
			
			
			// location
			var html = '';
			//html += JSON.encode(marker.place);
			var el = tabgrp.tabs[2].retrieve('section');
			var html = this.requestNearByWikipedia(latlng, el);
			
			//tabgrp.tabs[2].retrieve('section').empty().set('html', html);
			
			
			// text
			tabgrp.tabs[3].retrieve('section').empty().set('html', marker.descr);
			
			
			// image(s)
			var sec = tabgrp.tabs[4].retrieve('section');
			sec.empty();
			
			var hasImage = marker.image == '' ? false : true;	
			
			if (hasImage) {
				
				
				
				
				var path = './scripts/phpThumb/phpThumb.php?';
				var ul = new Element('ul', { id: 'picCarousel' });
				sec.adopt(ul);
				
				new Element('div', { id: 'carouselBtnContainer' }).adopt (
					new Element('ul', {id: 'btnCarousel', 'class' : 'clearfix'})
				).inject(sec);
				//var btn = new Element('ul', {id: 'btnCarousel', 'class' : 'clearfix'});
				
				var btn = $('btnCarousel');
				
				//sec.adopt(btn);
				
				$A(marker.image.split(',')).each(function(item, index){
						// phpthumb
						var optsSmall =  new Hash({
							src:	'../../' + item,
							zc:		1,		// zoom crop
							w: 		300,	// width
							h: 		200		// height
						});
						
						var optsLarge = new Hash({
							src:	'../../' + item,
							zc:		1,		// zoom crop
							wp:		480,	// max. width portrait
							hp:		640,	// max. height portrait
							wl:		640,	// max. width landscape
							hl:		480		// max. height landscape
						});	
						
						new Element('li', {'class' : 'slide'}).adopt(
																	 
							new Element('a', {
								href:	path + optsLarge.toQueryString(),			
								rel:	'carouselLighbox[set]'
							}).adopt(										 
								new Element('img', {
									src: path + optsSmall.toQueryString(),
									width: 300,
									height: 200,
									title:	'Click to enlarge...'
								})
							)
						).inject(ul)
					
					btn.adopt(
						new Element('li', { 'class': 'button', text: index+1 })		  
					)
				}) 
				
				// set carousel
				var cl = new SimpleCarousel(sec, $$('#picCarousel li.slide'), sec.getElements('li.button'), {
					slideInterval: 2000,
					rotateAction: 'click',
					autoplay: false
				});
				
				// lightbox
				var lb = new Lightbox({ relString: 'carouselLighbox', zIndex: 20000 }, sec.getElements('a'));
			} else {
				tabgrp.disable(4);	
			}
			
			//carousel.autoplay();
			
			
			/*
			
			var path = './scripts/phpThumb/phpThumb.php?';
			var opts_picSmall = new Hash({
				src:	'../../' + marker.image,
				zc:		1,		// zoom crop
				w: 		300,	// width
				h: 		200		// height
			});

			var opts_picLarge = new Hash({
				src:	'../../' + marker.image,
				zc:		1,		// zoom crop
				wp:		480,	// max. width portrait
				hp:		640,	// max. height portrait
				wl:		640,	// max. width landscape
				hl:		480		// max. height landscape
			});
			
			var secEl = tabgrp.tabs[4].retrieve('section').empty();
			secEl.adopt(
				new Element('a').setStyle('cursor','pointer').setProperties({
					href:	path + opts_picLarge.toQueryString(),
					rel: 	'markerImageLightbox'
				}).adopt(
					new Element('img').setProperty('src', path + opts_picSmall.toQueryString())
				)
			);
			
			new Lightbox({ relString: 'markerImageLightbox', zIndex: 20000 }, secEl.getElements('a'));
			*/
			
			// options
			var html = '';
			var keys = this.iconKeys[marker.optsIco.type];
			var opts = marker.optsIco.filter(function(value, key) {
				return keys.indexOf(key) != -1;											 
			});
											
			html += '<h3>Icon</h3><p class="code nopre">'   + (JSON.encode(opts).replace(/:/g, ': ').replace(/,/g, ', ')) + '</p>';
			html += '<h3>Marker</h3><p class="code nopre">' + (JSON.encode(marker.optsOvl).replace(/:/g, ': ').replace(/,/g, ', ')) + '</p>';
			tabgrp.tabs[5].retrieve('section').empty().set('html', html);
		},
		

		setCloseUpMap: function(marker) {
			var type 	= this.map.getCurrentMapType();
			var zoom	= Math.min(type.getMaximumResolution(), (this.map.getZoom() + 1));
			var opts	= { usageType: 'o', suppressCoypright: true, logoPassive: true, size: new GSize(300,200) };
			var latlng	= marker.ovl.getLatLng(); 
			if (!$defined(this.cmap)) {
				var el = this.tabs[6].tabs[1].retrieve('section');
				el.empty();
				new Element('div', {
					styles: {
						width: 300,
						height: 200,
					}		
				}).inject(el);
				
				this.cmap = new GMap2(el.getFirst(), opts);
				this.cmap.addControl(new GSmallZoomControl());
				
				
				// remove copyright
				var poweredby = this.cmap.getContainer().getElements('img[src$=poweredby.png]')[0];
				if (poweredby) { poweredby.hide(); }
				
				var terms = this.cmap.getContainer().getElements('a[href$=terms_maps.html]')[0];
				if (terms) { terms.hide(); terms.getPrevious().hide(); }

				
				// add events
				// center to marker on zoom
				GEvent.addListener(this.cmap, 'zoomend', function(currentZoom, newZoom) {			
					this.cmap.setCenter(latlng, newZoom);
				}.bind(this));
			}
			
			this.cmap.clearOverlays();
			this.cmap.setCenter(latlng, zoom, type);
			var clone = marker.ovl.copy();
			clone.disableDragging();
			this.cmap.addOverlay(clone);
		},


		// fn: onSortMarkers
		onSortMarkers: function(sorted) {	
			dbug.log('fn: onSortMarkers');
			var markers = [];
			$each(sorted, function(item) {
				var marker = this.markers.filter( function(marker) { return marker.get('id') == item.replace('li_', '') } )[0];						  
				if (marker)	 markers.push(marker);			  
				//var marker = this.markers.filter( function(marker) { return marker.elList == $(item) } )[0];				  
			}.bind(this));
			
			this.markers = markers;
			if (this.markers.length > 0) this.replaceMarker();
			this.replaceMarker();
			
			this.isSortingMarkers = false;
		},
		
		
		// set marker label
		prepareLabel: function(options, index) {
			// todo: check index values
			var limit 	= this.options.charLimit;
			var type	= (options.labelKeep) ? 3 : options.labelType;
			var label	= '';
			if (!$defined(index)) index = this.markers.length;
			
			if (!options.labelAdd) type = 4;
			
			switch (parseInt(type)) {
				case 0: // Number	
					label = (index+1).toString();
					break;
				case 1: // Letter
					var max = 26;
					var cnt = parseInt(index/max);
					var idx = index % max;					
					var str = ((cnt > 0) ? String.fromCharCode("A".charCodeAt(0) + cnt-1) : '') + String.fromCharCode("A".charCodeAt(0) + idx);
					label = (options.labelTransform) ? str.toUpperCase() : str.toLowerCase();	
					break;
				case 2: // Custom
					var str =  $('txtLabelText').get('inputValue');
					label = (options.labelTransform) ? str.toUpperCase() : str.toLowerCase();	
					break;				
				case 3: // Keep existing label
					label = this.markers[index-1].label;
					break;
				case 4: // None				
					label = '';
					break;
				default:
					label = '';					
			};
			
			return label.substring(0, limit);
		},	


		
		// get marker icon options
		getIconOptions: function() {		
			var dat 	= this.getFormFields(this.frmMarkerSettings);
			var options	= new Hash({
				// type, shape
				type				: this.toIconType(dat.radIconType), 	// marker, labledmarker, flat
				shape				: this.toIconShape(dat.radIconType),	// circle, roundrect
			
				// sizees
				width 				: dat.txtIconWidth,
				height				: dat.txtIconHeight,
				alternateSize		: dat.radAltSize, 
				
				// icon colors
				primaryColor		: dat.txtIconPrimaryColor,			
				alternateColor		: dat.txtIconAltColor,		
				cornerColor			: dat.txtIconCornerColor,
				strokeColor			: dat.txtIconStrokeColor,
					
				// label
				labelAdd			: dat.chkAddLabel,
				labelType			: dat.radLabelType,
				labelKeep			: dat.chkKeepLabel,
				label				: '', // dat.txtLabelText
				labelTransform		: dat.chkLabelToUppercase,
				labelSize			: dat.txtLabelSize,
				labelColor			: dat.txtLabelColor,
				
				// star
				addStar				: dat.chkAddStar,
				starPrimaryColor	: dat.txtStarPrimaryColor,
				starStrokeColor		: dat.txtStarStrokeColor,			
			});
			
			return options;
		},
		
		
		// fn: createPointer - helper marker
		createPointer: function() {
			if (!this.options.pointer) return false;
			// set icon
			var opt = {
				type			: 'flat',
				shape			: 'circle',
				width			: 16,
				height			: 16,
				primaryColor	: this.options.pointerColor.replace('#', '')
			}
			
			var index	= -1;
			var oIcon	= this.createIcon(index, opt);
			var opt		= null;
			
			// set pointer (marker)
			var latlng	= this.map.getCenter();
			latlng.y 	= latlng.y.round(6);
			latlng.x 	= latlng.x.round(6);
			var uid		= new Date().getTime();
			
			var opt		= {
				id:				uid,
				icon: 			oIcon.icon,
				title: 			latlng.y +', '+ latlng.x,
				dragCrossMove: 	false,
				clickable: 		false,
				draggable: 		true,
				bouncy: 		false,
				bounceGravity: 	1,
				autoPan: 		true,
				zIndexProcess:	$empty
			};
			
			var pointer	= new Hash();
			pointer.ovl	= new GMarker( latlng, opt );
			
			this.map.addOverlay(pointer.ovl);
			pointer.ovl.hide();
			pointer.elTips	= $(this.options.idPrefix + uid); // needs to be set after placing overlay - otherwise element is not available yet!
			
			this.attachMarkerEvents(pointer.ovl);
			return pointer;
		},
		
		
		// fn: updatePointer - helper marker
		updatePointer: function(latlng) {
			if (!this.options.pointer) return false;
			if (!$defined(latlng)) return false;
			
			this.pointer.ovl.setLatLng(latlng);
			this.pointer.elTips.set('title', latlng.y + ', ' + latlng.x);
			return this.pointer;
		},
		
		
		// fn: createIcon
		createIcon: function(index, options) {
			if (!$defined(index)) { dbug.log('Error: failed to create marker icon - index is missing') }
			if (!$defined(options)) options = this.getIconOptions();
			
			options.label	= this.prepareLabel(options, index);
			if (options.type == 'marker') {
				var icon = MapIconMaker.createMarkerIcon(options);
			} else if (options.type == 'labeledmarker') {			
				var icon = MapIconMaker.createLabeledMarkerIcon(options);
			} else if (options.type == 'flat') {
				var icon = MapIconMaker.createFlatIcon(options);
			}
			
			
			var oIcon = { icon: icon, options: options	}
			if (index == 0) this.updateIconPreview(oIcon.icon);
			return oIcon;
		},

	
		// fn: replaceMarker - re-create icon AND overlay
		// param: array of markers
		replaceMarker: function(aMarkers) {
			if (!$defined(aMarkers)) aMarkers = this.markers;
			dbug.log(aMarkers.length);
			this.editMode = true;
			
			$each(aMarkers[0], function(item, index) {
				dbug.log(index +': '+ item);						 
			})
			
			
			$each(aMarkers, function(item){
				index	= this.markers.indexOf(item);
				latlng	= item.ovl.getLatLng();
				image	= item.image;
				dbug.log('replaceMarker: ' + image);
				dat		= new Hash({ txtLat: latlng.y, txtLng: latlng.x, txtImage: image });
				this.currentMarker = item;
				this.map.removeOverlay(item.ovl);
				marker	= this.setMarker(dat, index, item);
				this.addMarker(marker);
			}.bind(this))
			this.editMode = false;
		},
		
		
		// fn: updateIconPreview - set default icon
		updateIconPreview: function(icon) {
			if (!$defined(icon)) return false;
			this.previewIcon.set('src', icon.image);
			return icon;
		},
		
	
		// fn: setDemoMarker
		setDemoMarker: function() {
			if (!this.options.demo) return false;
			var dat				= {};
			var bounds 			= this.map.getBounds();
			var southWest 		= bounds.getSouthWest();
			var northEast 		= bounds.getNorthEast();
			var lngSpan 		= northEast.lng() - southWest.lng();
			var latSpan 		= northEast.lat() - southWest.lat();
			var num				= 5; // number of markers to create
			
			dat.txtZoom			= this.map.getZoom();
			for (var i = 0; i < num; i++) {
				dat.txtLat 		= (southWest.lat() + latSpan * Math.random());
				dat.txtLng 		= (southWest.lng() + lngSpan * Math.random());
				dat.txtTitle	= '';
				dat.txtImage	= '';
				marker 			= this.setMarker(dat, i);
				if (!$chk(marker)) { dbug.log('Error: unable to set marker'); return false; }
				this.addMarker(marker);	
			}
			this.onCompleteAddMarker();
		},


		// fn: setMarker
		setMarker: function(dat, index, marker) {
			dbug.log('fn setMarker: ' + dat.txtImage);
			if (!$defined(dat)) { dbug.log('Error: no marker data provided'); return false; }
			if (!$defined(index)) index = this.markers.length;
			
			var prefix	= this.options.idPrefix;
			var lat		= parseFloat(dat.txtLat).round(6);
			var lng		= parseFloat(dat.txtLng).round(6);
			var latlng	= new GLatLng(lat, lng);
			
			var marker	= this.editMode ? this.currentMarker : new Hash();
			var uid		= this.editMode ? marker.get('id').replace(prefix, '') : $time();
			
			// set marker icon AND label
			var oIcon	= this.createIcon(index);
			var icon	= oIcon.icon;
			icon.image	= icon.image +'&id=' + prefix + uid; // add unique id icon image url
			var label	= oIcon.options.label;
			
			var draggable = $('chkAllowDrag').get('inputValue');
			
			var options	= {
				id:				uid,
				icon: 			icon,
				title: 			dat.txtTitle || latlng.y + ', ' + latlng.x,
				dragCrossMove: 	false,
				clickable: 		true,
				draggable: 		true,
				bouncy: 		false,
				bounceGravity: 	1,
				autoPan: 		true,
				zIndexProcess:	$empty
			};
			
			// set marker
			marker.ovl			= new GMarker(latlng, options );
			marker.id			= prefix + uid;
			marker.alt			= dat.txtAlt || 'n/a';
			marker.lat			= latlng.y;
			marker.lng			= latlng.x;
			marker.latlng		= latlng;
			marker.zoom			= dat.txtZoom,
			marker.label		= label;
			marker.title		= options.title;
			marker.descr		= dat.txtDescr || '';
			dbug.log('fn setMarker: ' + dat.txtImage);
			marker.image		= dat.txtImage;
			marker.showImage	= $('chkShowImage').get('inputValue');	
			marker.timeToPrev	= 0;
			marker.timeToStart	= 0;
			marker.distToPrev	= 0;
			marker.distToStart	= 0;
			marker.elIcon		= false;
			marker.elTips		= false;
			marker.elList		= this.editMode ? marker.elList : false;
			marker.place		= false;
			marker.optsIco		= oIcon.options;
			marker.optsOvl		= $H(options).erase('icon');
			marker.htmlTips		= latlng.y + ', ' + latlng.x;	
			marker.htmlInfo		= '';

			return marker;
		},


		// fn: addMarker - add marker to map
		addMarker: function(marker, onComplete) {
			if (!$defined(marker)) { dbug.log('Error: unable to add marker - no valid marker provided'); return false; }
			this.map.addOverlay(marker.ovl);
			this.attachMarkerEvents(marker.ovl);

			// add marker to list
			if (!this.editMode) {
				marker.elList = this.addMarkerToList(marker);	
				this.markers.push(marker);
			}
			
			if (this.options.demo) this.getAddressByLatLng(marker); 
			var showTip = $('chkShowTip').get('inputValue');
			this.chains.addMarker.chain(
				function(els) {
					marker.elIcon = els.imgEl;
					marker.elTips = els.tipEl;	
					//marker.ovl.setImage(els.imgEl.get('src').replace('&id='+marker.get('id') +'', ''));
					marker.elTips.set('title', marker.title); // title and image element do not exist upon placing marker overlay to map
					if (showTip) this.enableTipMarker(marker);
				}.bind(this)
			);
			
			this.getMarkerElements(marker); // retrieve marker image element and tooltip element
			
			// onComplete addMarker
			if ($defined(onComplete)) onComplete();
		},
		
		
		// fn: setMarkerTip
		setMarkerTip: function(marker) {
			if (!$defined(marker)) return false;
			var latlng = marker.ovl.getLatLng();
			var html = '';
			html += '<dl>';
			html += '	<dt><label>Altitude:</label>'	+ marker.alt						+ '</dt>';
			html += '	<dt><label>Latitude:</label>'	+ this.convert(latlng.y, 'lat')	+ '</dt>';
			html += '	<dt><label>&nbsp;</label>'		+ latlng.y 							+ '</dt>';
			html += '	<dt><label>Longitude:</label>'	+ this.convert(latlng.x, 'lng') 	+ '</dt>';
			html += '	<dt><label>&nbsp;</label>'		+ latlng.x 							+ '</dt>';
			html += '</dl>';
			return html;
		},
		
		
		// fn: getMarkerElements - needed to highlight marker on hover/edit and set marker title attribute
		getMarkerElements: function(marker, count) {
			count = $defined(count) ? count : 0;
			var id = marker.get('id');
			
			try {
				var el = this.mapCanvas.getElements('img[src$=' + id + ']')[0];
				if (!$chk(el)) { throw 'error'; } // el not found yet	
				var els = { imgEl: el, tipEl: $(id) };
				this.chains.addMarker.callChain(els);
			} catch(e) {				
				if (e == 'error' && count < 5) { count++; this.getMarkerElements.delay(250, this, [marker, count]) };
			}
		},

		
		// fn: onClickAddMarker 
		onClickAddMarker: function() {
			var dat = this.getFormFields(this.addMarkerForm);
			////console.log('numeric: ' + /^-?(?:0$0(?=\d*\.)|[1-9]|0)\d*(\.\d+)?$/.test(lng));
			if (dat.txtLat == '' || dat.txtLng == '') { dbug.log('invalid latitude/longitude'); return false; }
			var marker = this.setMarker(dat);
			if (!$chk(marker)) { dbug.log('error setting marker'); return false; }
			
			var onComplete = this.onCompleteAddMarker.bind(this);
			
			this.addMarker(marker, onComplete);
		},
		
		
		// fn: onClickCancel AddMarker - cancelled add marker
		onClickCancelAddMarker: function() {
			this.addMarkerForm.reset();
			if (this.options.pointer) this.pointer.ovl.hide();
		},
		
		
		// fn: onCompleteAddMarker
		onCompleteAddMarker: function() {
			this.onClickCancelAddMarker();
			this.setPolyline();
			this.centerMapToMarkers();
			this.removeAllPicturesFromList();
		},
		
		
		// fn: onBeforeEditMarker
		onBeforeEditMarker: function(marker) {
			dbug.log('fn: onBeforeEditMarker');
			if (this.editMode) return false;
			if (!$defined(marker)) return false;
			
			this.addMarkerForm.reset();
			if (this.options.pointer) this.pointer.ovl.hide();
			
			this.editMode		= true;
			this.currentMarker	= marker;
			this.markerBackup	= this.backupMarker(marker);
			
			this.disableDragMarker();
			
			// populate form data
			this.objectToFormData(this.addMarkerForm, marker);
			// open accordion section
			if (!this.accordions[0].previous == 1) this.accordions[0].display(1);
			
			// set buttons
			$('submitAddMarkerForm').erase('disabled').addClass('edit').set('value','Save');		
			
			// highlight list item
			marker.elList.addClass('active');
		},
		
		
		// fn: onClickEditMarker - save changes
		onClickEditMarker: function() {
			dbug.log('fn: onClickEditMarker');
			var dat = this.getFormFields(this.addMarkerForm);	
			var marker = this.setMarker(dat);
			if (!$chk(marker)) { dbug.log('error setting marker'); return false; }
			var callback = this.onCompleteEditMarker.bind(this);
			this.addMarker(marker, callback);
		},
		
		
		// fn: onCompleteEditMarker
		onCompleteEditMarker: function() {
			if (!this.editMode) return false;
			//this.onClickCancelEditMarker();
			this.setPolyline();
			this.fireEvent('onChangeMap');	
		},
		
		
		// fn: onClickCancelEditMarker - reject changes
		onClickCancelEditMarker: function() {
			this.enableDragMarker();
			var marker = this.restoreMarker();
			this.replaceMarker($splat(marker));
			this.getAddressByLatLng(marker); 
			this.editMode 		= false;
			this.addMarkerForm.reset();
			this.removeAllPicturesFromList();
			this.dimMarkerReset();
			$('submitAddMarkerForm').removeClass('edit');		
			$('submitAddMarkerForm').set('value','Add');
			this.currentMarker.elList.removeClass('active');
			this.currentMarker	= null;
			this.markerBackup	= null;
		},
		
		
		// fn: backupMarker
		backupMarker: function(marker) {
			var oBackup = new Hash();
			$each(marker, function(value, key) {
				oBackup[key] = value;
			})
			oBackup.latlng = marker.ovl.getLatLng();
			return oBackup;
		},
		
		
		// fn: restoreMarker
		restoreMarker: function() {
			var marker = this.currentMarker;
			$each(marker, function(value, key) {
				marker[key] = this.markerBackup[key];							   
			}.bind(this))
			marker.ovl.setLatLng(this.markerBackup.latlng);
			return marker;
		},
		
		
		// fn: populate form
		objectToFormData: function(form, dat) {
			if (!$defined(form) || !$defined(dat)) return false;
			
			form.reset(); // empty form before populating data			
			
			if (form == this.addMarkerForm) {
				var marker = dat;
				var latlng = marker.ovl.getLatLng();
				
				// populate form fields from marker
				$('txtLat').set(  'value', latlng.y);
				$('txtLng').set(  'value', latlng.x);
				$('txtAlt').set(  'value', marker.alt);
				$('txtZoom').set( 'value', marker.zoom);
				$('txtLabel').set('value', marker.label);
				$('txtTitle').set('value', marker.title);
				$('txtDescr').set('value', marker.descr);
				
				dbug.log('fn objectToFormData: ' + marker.image);
				$('txtImage').set('value', marker.image);
				if (marker.image == '') return;
				marker.image.split(',').each(function(item, index) {
					this.addPictureToList(item);						   
				}.bind(this));
				
				return true;
			};
			
			return false;
		},
		
		
		// fn: onMousedownMarker
		onMousedownMarker: function(marker, latlng) {
			marker.elTips.fireEvent('mouseleave'); // hide marker tip
			return false;
		},
		
		
		// fn: onDragStartMarker
		onDragStartMarker: function(marker, latlng) {
			// todo: disable tooltip while dragging
			
			if (!$defined(marker)) return false;
			if ($('chkShowDirections').checked) { // hide directions while moving marker
				if (this.map.overlays.has('polyline')) this.map.overlays.polyline.hide();
			}
			
			if (marker == this.pointer) { $('txtTitle').set('value', ''); $('txtAlt').set('value', 'n/a'); }
			return false;
		},
		
		
		// fn: onDragMarker
		onDragMarker: function(marker, latlng) {
			if (!$defined(marker)) return false;
			latlng.y = latlng.y.round(6);
			latlng.x = latlng.x.round(6);
			marker.ovl.setLatLng(latlng);
			
			if (marker == this.pointer) {	// pointer
				$('txtLat').value = latlng.y;
				$('txtLng').value = latlng.x;
			} else {	// existing marker
				marker.elList.getFirst().set('text', latlng.y + ', ' + latlng.x);	
				if (!this.editMode) return false;
				
				// edit mode
				$('txtLat').value = latlng.y;
				$('txtLng').value = latlng.x;
			}
		},
		
		
		// fn: onDragEndMarker
		onDragEndMarker: function(marker, latlng) {
			if (!$defined(marker)) return false;	

			latlng.y = latlng.y.round(6);
			latlng.x = latlng.x.round(6);
			marker.ovl.setLatLng(latlng);
			
			var title = latlng.y + ', ' + latlng.x;
			marker.elTips.set('title', title);
			
			if (marker == this.pointer) { // pointer
				this.getAddressByLatLng(null, latlng);	
				this.updatePanoramaView(latlng);
				return false;
			} else {	// existing marker
				marker.alt 		= 'n/a';
				marker.title 	= title;
				marker.place	= false;
				marker.elList.getFirst().set('text', title);
				this.prepareTipMarker(marker);
				this.getAddressByLatLng(marker, latlng);
				this.setPolyline();
				
				this.fireEvent('onChangeMap');	
			}
		},
		
		
		// fn: prepareTipMarker
		prepareTipMarker: function(marker) {
			var el = marker.elTips;
			el.set('title', '');
			el.removeProperty('href');
			el.store('tip:title', marker.title);
			el.store('tip:text', this.setMarkerTip(marker));	
			
			return el;
		},
		
		
		// fn: enableTipMarker
		enableTipMarker: function(marker) {
			var markers = $defined(marker) ? $splat(marker) : this.markers;
			markers.each(function(item) {
				var tip = this.prepareTipMarker(item);
				this.tips.attach(tip);								 
			}.bind(this))
		},
		
		
		// fn: disableTipMarker
		disableTipMarker: function(marker) {
			var markers = $defined(marker) ? $splat(marker) : this.markers;
			markers.each(function(item) {
				this.tips.detach(item.elTips);				   
			}.bind(this))
		},
		
		
		// fn: toggleMarkerImage
		toggleMarkerImage: function(val) {
			showImage = $defined(val) ? val : false;
			var markers = this.markers;
			markers.each(function(item) {
				dbug.log(showImage);
				item.showImage = showImage;					  
			})
		},
		
		
		// fn: enableDragMarker
		enableDragMarker: function() {
			this.markers.each(function(item){
				if (item.optsOvl) item.ovl.enableDragging();
			});
		},
		
		
		// fn: disableDragMarker - disable dragging of inactive markers
		disableDragMarker: function() {
			this.markers.each(function(item){
				if (item.optsOvl) item.ovl.disableDragging();
			});
		},


		// fn: removeMarker - remove marker overlay from map
		removeMarker: function(marker) {
			if (!$defined(marker)) return false;
			this.map.removeOverlay(marker.ovl);
			return marker;
		},
		
		
		// fn: deleteMarker 
		deleteMarker: function(marker) {
			if (!$defined(marker)) return false;
			if (!confirm('Please confirm to delete marker - ' + marker.title + '?')) return false;
			
			marker.elList.set('tween', {
				onComplete: function() {
					//this.sortableMarkers.removeItems(marker.elList).destroy();	
					marker.elList.destroy();	
					this.removeMarker(marker);
					this.markers.erase(marker);
					this.replaceMarker();
					this.setPolyline();
					this.fireEvent('onChangeMap');	
					this.dimMarkerReset();
					this.centerMapToMarkers();
				}.bind(this)
			});
			marker.elList.highlight(this.options.highlight.remove);			
		},
		
		
		showAllMarkers: function() {
			$each(this.markers, function(item) {
				item.ovl.show();							 
			});		
		},
		
		
		hideAllMarkers: function(marker) {
			if (!$defined(marker)) return false;
			$each(this.markers, function(item) {
				if (item != marker)item.ovl.hide();							 
			});	
		},
		
		
		// GEOCODER
		// fn: requestAltitude using http://ws.geonames.org/
		requestAltitude: function(latlng) { // JSON
			if (!latlng) return false;
			var mode	= 'json';
			var args	= $H({ lat: latlng.y, lng: latlng.x, mod: mode, max: 3 });
			new Request({
				url: 		'./scripts/requestAltitude.php?',
				method: 	'post',
				autoCancel: true,
				useWaiter: 	true,
				timeout: 	this.options.requestTimeout,
				onTimeout:	function() {
					alt = 'n/a';
					this.chains.getAltitude.callChain(alt);
				}.bind(this),
				onSuccess: 	function(responseText, responseXML) {	
					this.chains.getAltitude.callChain(JSON.decode(responseText).alt);
				}.bind(this)			
			}).send( args.toQueryString() );
		},


		// fn: requestAltitude using http://ws.geonames.org/
		requestNearByWikipedia: function(latlng, targetEl) { // JSON
			if (!latlng) return false;
			var mode	= 'json';
			var args	= $H({ lat: latlng.y, lng: latlng.x, mod: mode, max: 2 });
			new Request({
				url: 		'./scripts/requestWikipedia.php?',
				method: 	'post',
				autoCancel: true,
				useWaiter: 	true,
				timeout: 	this.options.requestTimeout,
				onTimeout:	function() {
					var html = '';
					html += '<p><strong>Location information not available.</strong></p>';
					html += '<p>Request timed out... Please try again!</p>'
					targetEl.empty().set('html', html);
					//this.chains.getAltitude.callChain(alt);
				}.bind(this),
				onSuccess: 	function(responseText, responseXML) {	
					//this.chains.getAltitude.callChain(JSON.decode(responseText).alt);
					var response = JSON.decode(responseText);
					
					var arr = response.geonames;
					var html = '';
					$each(arr, function(item, index) {
						html += item.summary + '<br /> <br />';					
					});
					targetEl.empty().set('html', html);
				}.bind(this)			
			}).send( args.toQueryString() );
		},

		
		// fn: getLatLngByAddress
		getLatLngByAddress: function(dat) {
			if (!$defined(dat)) dat = this.getFormFields(this.geocoderForm);
			var address	= dat.txtAddress;
			if (address	== '') address = dat.txtStreet	+ ',' + dat.txtCity	+ ',' + dat.txtState	+ ',' + dat.txtZip	+ ',' + dat.txtCountry;	
			
			if (!$defined(this.map.geocoder)) this.map.geocoder = new GClientGeocoder();
			this.map.geocoder.getLatLng(address, function(latlng){
				this.getLatLngByAddressCallback(latlng, address);									
			}.bind(this));
		},
		
		
		// fn: getLatLngByAddressCallback
		getLatLngByAddressCallback: function(latlng, address) {
			if (!latlng) { dbug.log(address + ' not found'); return false; }	
			
			var zoom = this.map.getZoom();
			this.map.setCenter(latlng, zoom);
			
			// highlight
			var fx = new Fx.Tween('txtAddress', {
				duration: 2000,
				onComplete: function() {							
					this.geocoderForm.reset();
					this.onClickMap(null, latlng);
					this.tabs[1].show(0); // open add/edit marker tab
				}.bind(this)						
			}).start('background-color', this.options.highlight.success, this.options.highlight.main);
		},
		
		
		// fn: getAltitudeByLatLng
		getAltitudeByLatLng: function(latlng) {
			return this.requestAltitude(latlng);	
		},
		
		
		// fn: getAddressByLatLng
		getAddressByLatLng: function(marker, latlng) {
			if (!this.options.requestLocation || this.isSortingMarkers) return false; // request location is disabled
			
			if (!$defined(this.map.geocoder)) this.map.geocoder = new GClientGeocoder();
			var latlng = $chk(marker) ? marker.ovl.getLatLng() : latlng;
			this.map.geocoder.getLocations(latlng, function(response) {
				this.getAddressByLatLngCallback(marker, latlng, response);											
			}.bind(this));
		},
		
		
		// fn: getAddressByLatLngCallback
		getAddressByLatLngCallback: function(marker, latlng, response) {
			// comment:
			// error codes: G_GEO_SUCCESS (200), G_GEO_BAD_REQUEST (400), G_GEO_SERVER_ERROR (500), G_GEO_MISSING_QUERY (601), G_GEO_MISSING_ADDRESS (601), G_GEO_UNKNOWN_ADDRESS (602), G_GEO_UNAVAILABLE_ADDRESS (603), G_GEO_UNKNOWN_DIRECTIONS (604), G_GEO_BAD_KEY (610), G_GEO_TOO_MANY_QUERIES (620)
			
			// accuracy code: 0 = unknown location, 1 = country level accuracy, 2 = region (state, province, etc.), 3 = sub-region (county, municipality, etc.), 4 = Town (city, village), 5 = zip code, 6 = street level, 7 = intersection level, 8 = address level, 9 = premise (building name, property name, shopping center, etc.)
			
			// json format: see http://code.google.com/apis/maps/documentation/services.html#Geocoding
			/* JSON response
				{
				  "name": "1600 Amphitheatre Parkway, Mountain View, CA, USA",
				  "Status": {
					"code": 200,
					"request": "geocode"
				  },
				  "Placemark": [
					{
					  "address": "1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA",
					  "AddressDetails": {
						"Country": {
						  "CountryNameCode": "US",
						  "AdministrativeArea": {
							"AdministrativeAreaName": "CA",
							"SubAdministrativeArea": {
							  "SubAdministrativeAreaName": "Santa Clara",
							  "Locality": {
								"LocalityName": "Mountain View",
								"Thoroughfare": {
								  "ThoroughfareName": "1600 Amphitheatre Pkwy"
								},
								"PostalCode": {
								  "PostalCodeNumber": "94043"
								}
							  }
							}
						  }
						},
						"Accuracy": 8
					  },
					  "Point": {
						"coordinates": [-122.083739, 37.423021, 0]
					  }
					}
				  ]
				}			
			*/
			if (!response || response.Status.code != 200) { dbug.log('Error: address for ' + latlng + ' not found'); return false; }
			var place = response.Placemark[0];
			//dbug.log(JSON.encode(place));
			// update latlng - snap to location
			if (this.options.snapToLocation) { // use coordinates of closest address
				var lat	= place.Point.coordinates[1].round(6);
				var lng	= place.Point.coordinates[0].round(6);
				latlng	= new GLatLng(lat, lng);
			}
			
			// update geodata
			var update = function (alt) {
				var tname	= '';
				if ($defined(place.AddressDetails.Country.AdministrativeArea) && $defined(place.AddressDetails.Country.AdministrativeArea.Locality) && $defined(place.AddressDetails.Country.AdministrativeArea.Locality.Thoroughfare)) {
					tname = place.AddressDetails.Country.AdministrativeArea.Locality.Thoroughfare.ThoroughfareName;	
				};
				var address	= place.address || 'n/a';
				var title	= (tname != '') ? tname : address;
				var dat		= { 'alt': alt, 'latlng': latlng, 'place': place, 'title': title, 'address': address };
				
				// process geodata
				if ($chk(marker)) {
					this.placeDataToMarker(marker, dat);	
					if (this.editMode) this.placeDataToForm(dat);
					if (this.options.snapToLocation) marker.ovl.setLatLng(latlng);
				} else {	// new marker OR pointer
					this.placeDataToForm(dat);
					if (this.options.snapToLocation) this.updatePointer(latlng);
				}
			}
			
			// init update geodata
			if (this.options.requestAltitude) {
				this.chains.getAltitude.chain(
					function(alt) {
						var boundUpdate = update.bind(this, alt);
						boundUpdate();
					}.bind(this)					   
				);
				
				this.getAltitudeByLatLng(latlng);	// get altitude - ajax request
			} else {	// unable to get altitude through Google at the moment
				var alt	= (place.Point.coordinates[2] == 0) ? 'n/a' : place.Point.coordinates[2];
				var boundUpdate = update.bind(this, alt);
				boundUpdate();
			}
		},
		
		
		// fn: placeDataToForm
		placeDataToForm: function(dat) {
			if (!$defined(dat)) return false;
			$('txtAlt').set('value', dat.alt);
			$('txtLat').set('value', dat.latlng.y);
			$('txtLng').set('value', dat.latlng.x);
			$('txtAlt').store('place', dat.place);
			$('txtTitle').set('value', dat.title);	
		},
		
		
		// fn: placeDataToMarker
		placeDataToMarker: function(marker, dat) {
			if (!$defined(marker) || !$defined(dat)) return false;
			marker.alt 		= dat.alt;							
			marker.title	= dat.title;
			marker.place 	= dat.place;
			marker.elList.getFirst().set({ 'text': marker.title, 'title': dat.address });
			//if ($chk(marker.elTips)) marker.elTips.set('title', marker.title);
			if ($chk(marker.elTips)) this.prepareTipMarker(marker);
		},
		
		
		// value conversions
		convert: function(value, type, dec) {
			var dec = (!$defined(dec) ? 2 : dec);
			switch(type) {
				case 'HMS':		// seconds to hours, minutes, seconds				
					var s = parseInt(value % 60 );
					var m = parseInt((value/60) % 60 );
					var h = parseInt((value/3600) % 24 );
					return ((h > 0 ) ? h + ((h > 1) ? ' hours' : ' hour') + ' ' : '') + m + ((m > 1) ? ' mins' : ' min');
					break;
				case 'km': 		// meters to km				
					return (value/1000).toFixed(dec)
					break;
				case 'ft':	// meters to feet
					return (value*3.28084).toFixed(dec);
					break;
				case 'mi':	// meters to miles				
					return (value*0.000621371192237334).toFixed(dec);
					break;
				case 'lat': case 'lng':				
					function DegreeToDMS(deg, type) {
						if (!deg.toString().match(/[0-9]/)) { return ''; }
						var dir = (type == 'lat') ? parseFloat(deg < 0) ? 'S' : 'N' : parseFloat(deg < 0) ? 'W' : 'E';
						var d = Math.floor(Math.abs(parseFloat(deg)));
						var mmm = 60 * (Math.abs(parseFloat(deg)) - parseFloat(d))
							mmm = Math.round(1000000 * mmm) / 1000000;
						var m = Math.floor(parseFloat(mmm));
						var s = 60 * (parseFloat(mmm) - parseFloat(m))
						s = Math.round(1000 * s) / 1000;
						return dir + ' ' + d + String.fromCharCode(176) + ' ' + m + '\'' + ' ' + commaToPoint(s) + '"';
					};
	
					function commaToPoint (num) {
						num = num + ''; // number to string
						return (num.replace(/,/g,'.'));
					};
					
					value = value.toString();
					var isNegative = (value.match(/(^-|[WS])/i)) ? true : false;
					
					value = value.replace(/[NESW\-]/gi,' ');
					if (!value.match(/[0-9]/i)) { return false; }
					parts = value.match(/([0-9\.\-]+)[^0-9\.]*([0-9\.]+)?[^0-9\.]*([0-9\.]+)?/);
					if (!parts || parts[1] == null) { return false; }
						
					n = parseFloat(parts[1]);
					if (parts[2]) { n = n + parseFloat(parts[2])/60; }
					if (parts[3]) { n = n + parseFloat(parts[3])/3600; }
					if (isNegative && n >= 0) { n = 0 - n; }
				
					return commaToPoint(DegreeToDMS(n, type));
					break;
				default:
					return null;
			}		
		},
		
		
		// fn: addQuotesToString
		addQuotesToString: function(value) {
			return ($type(value) == 'string') ? (value.toFloat()) ? value : '\'' + value + '\'' : value;	
		},
		
		
		// fn: setDynamicMapCode
		setDynamicMapCode: function() {
			var json 	= new Hash({ maps: [], markers: [] });
			var fldCode	= $('mapCode').getFirst();
			var strCode	= '';
			
			// prepare map settings
			var dat = this.getFormFields(this.frmMapSettings);
			var keys = []; // keys to ignore
			json.maps.push( dat.filter(function(value, key) { return keys.indexOf(key) == -1; }) );
			
			strCode += JSON.encode(json.maps[0]).replace(/:/g, ': ').replace(/,/g, ', ') + '<br /><br />';
			
			
			// prepare marker settings
			var keys = ['ovl', 'id', 'latlng', 'elIcion', 'elTips', 'elList', 'place', 'htmlTips', 'htmlInfo']; // keys to ignore
			this.markers.each(function(item, index) {
				json.markers.push( item.filter(function(value, key) { return keys.indexOf(key) == -1; }) );
				strCode += JSON.encode(json.markers[index]).replace(/:/g, ': ').replace(/,/g, ', ') + '<br /><br />';
			});
			
			// update code field
			fldCode.set('html', strCode);
			return json;
		},
		
		
		// fn: browsePictures
		browsePictures: function() {			
			var url		= './scripts/browse.php';
			var mode	= 'pictures';	// 'pictures' OR 'list'
			
			if (!this.pictureBrowser) {
				this.pictureBrowser = new StickyWinModal.Ajax({
					//url: './scripts/browse.php',
					wrapWithUi: false,
					//caption: 'Select picture',
					showNow: false,					
					modalOptions: {
						modalStyle: {
							'background-color': '#efefef',
							'opacity': 0.6
						}
					},
					handleResponse: function(response){
						this.pictureBrowser.setContent(StickyWin.ui('Add picture to marker', response));
						var els = this.pictureBrowser.win.getElements('a');
						els.each(function(item, index) {
							item.addEvent('click', function(ev){ 
								ev.stop();
								if (!this.addPictureToList(item.get('href'))) this.pictureBrowser.hide();
							}.bind(this));
							
							if (mode == 'pictures') return false;
							// set tip
							item.store('tip:title', '');
							item.store('tip:text', '<img src="' + item.get('href') + '" width="90" height="60">');
						}.bind(this));
						this.tips.attach(els);
					}.bind(this)
				});
			}
			
			// update
			this.pictureBrowser.update(url, { data: { mode: mode } }).show();
		},
		
		
		// fn: addPictureToList
		addPictureToList: function(imgUrl) {
			var max = 4;
			
			var el = $('loadFileManager');
			if (!$chk($('markerPictureList'))) {
				el.adopt(new Element('ul', { id: 'markerPictureList' }));	
			}
			
			var children = $('markerPictureList').getChildren();
			var found = children.filter(function(item){
				//return item.get('text') == imgUrl;	
				return item.retrieve('imgUrl') == imgUrl;		
			}).length;
			if (found > 0) return false; // already added
			
			var imgName = imgUrl.substring(imgUrl.lastIndexOf('/')+1);
			var el = $('markerPictureList');
			el.adopt(
				new Element('li', { text: imgName }).adopt( 
					new Element('a', {
						'class' 	: 'btn remove',
						'title' 	: 'Remove picture',
						'href'		: '#',
						'events'	: {
							'click'	: function(ev) {
								ev.stop();
								this.onRemovePictureFromList(ev.target.getParent());
							}.bind(this),
							'mouseenter' : function(ev) {
								ev.stop()
							},
							'mouseleave' : function(ev) {
								ev.stop()
							}
						}			
					})
				).store('imgUrl', imgUrl)
			)
			
			var num = this.onAddPictureToList();
			
			
			// check max
			if (num == max) return false;
			
			return true;
		},
		
		
		// fn: onAddPictureToList
		onAddPictureToList: function() {
			// todo: update marker.pictures
			var ul	= $('markerPictureList');
			var lis	= ul.getChildren();
			var pics = [];
			lis.each(function(item, index){
				pics.push(item.retrieve('imgUrl'));
			})
			
			var fldImages = $('txtImage');
			fldImages.set('value', pics);
			return lis.length;
		},
		
		
		// fn: onRemovePictureFromList
		onRemovePictureFromList: function(el) {
			// todo: update marker.pictures	
			el.destroy();
			var ul	= $('markerPictureList');
			var lis	= ul.getChildren();
			var pics = [];
			lis.each(function(item, index){
				pics.push(item.retrieve('imgUrl'));
			})
			
			var fldImages = $('txtImage');
			fldImages.set('value', pics);
		},
		
		removeAllPicturesFromList: function() {
			var el = $('markerPictureList');
			if (el) el.empty();
		},
		
		
		// fn: browseFiles
		browseFiles: function() {			
			var url		= './scripts/browseFiles.php';
			var mode	= 'pictures';	// 'pictures' OR 'list'
			
			if (!this.pictureBrowser) {
				this.pictureBrowser = new StickyWinModal.Ajax({
					wrapWithUi: false,
					showNow: false,					
					modalOptions: {
						modalStyle: {
							'background-color': '#efefef',
							'opacity': 0.6
						}
					},
					handleResponse: function(response){
						this.pictureBrowser.setContent(StickyWin.ui('Select KML/XML file', response));
						var els = this.pictureBrowser.win.getElements('a');
						els.each(function(item, index) {
							item.addEvent('click', function(ev){ 
								ev.stop();
								$('txtFile').set('value', item.get('href').substr(item.get('href').indexOf('/')+1));
								this.pictureBrowser.hide();
							}.bind(this));
							
							//if (mode == 'pictures') return false;
							// set tip
							//item.store('tip:title', item.get('href'));
							item.store('tip:text', item.get('href'));
							//item.store('tip:text', '<img src="' + item.get('href') + '" width="90" height="60">');
						}.bind(this));
						this.tips.attach(els);
					}.bind(this)
				});
			}
			
			// update
			this.pictureBrowser.update(url, { data: { mode: mode } }).show();
		},
		
		// fn: onClickAddXMLFile
		onClickAddXMLFile: function(fName) {
			var fPath = './files/';
			fName = fPath + fName;
			
			GDownloadUrl(fName, function(data, responseCode) {
				var xml = GXml.parse(data);
			  	var markers = xml.documentElement.getElementsByTagName('Point');
			  
			  	$each(markers, function(item) {
					var coordinates = item.getElement('coordinates').get('text').split(',');
					var lat	= parseFloat(coordinates[1]);
					var lng	= parseFloat(coordinates[0]);
					var zmn	= parseFloat(coordinates[2]);
					
					//dbug.log(lat + ' || '+ lng + ' || '+ zmn);
					
					
					var dat = {};	
					dat.txtLat 		= lat;
					dat.txtLng 		= lng;
					dat.txtTitle	= '';
					dat.txtImage	= '';
					var marker 		= this.setMarker(dat, this.markers.length);
					if (!$chk(marker)) { dbug.log('Error: unable to set marker'); return false; }
					this.addMarker(marker);	
				}.bind(this))
			}.bind(this));	
		}
	}); 
	
	
	// EXTENSIONS
	// extend TabSwapper
	var TabSwapper = new Class({
		Extends: TabSwapper,
		options: {
			disabledClass	: 'disabled',	
			onEnable		: $empty,
			onDisable		: $empty,
		},
		enable: function(indices) {
			if (!$defined(indices)) return this;
			
			indices = ($type(indices) != 'array') ? [indices] : indices;
			
			indices.each(function(idx) {
				if (idx != this.now) {
					var tab = this.tabs[idx];
					if (!tab) return this;
					var sect = tab.retrieve('section');
					if (!sect) return this;
					tab.removeClass(this.options.disabledClass).setStyle('cursor', 'pointer');				
					tab.retrieve('clicker').addEvent('click', function() {
						this.show(idx);
					}.bind(this));
					this.fireEvent('onEnable', [idx, sect, tab]);
				}						  
			}.bind(this));
			return this;
		},
		disable: function(indices) {
			if (!$defined(indices)) return this;			
			indices = ($type(indices) != 'array') ? [indices] : indices;			
			indices.each(function(idx) {
				if (idx != this.now) {
					var tab = this.tabs[idx];
					if (!tab) return this;
					var sect = tab.retrieve('section');
					if (!sect) return this;
	
					tab.addClass(this.options.disabledClass).setStyle('cursor', 'default');					
					tab.retrieve('clicker').removeEvents('click');
					this.fireEvent('onDisable', [idx, sect, tab]);
				}						  
			}.bind(this));
			return this;
		},
		isActive: function(idx) {
			return (this.now == idx) ? true : false;	
		},
		current: function() {
			return this.now;	
		}
	});
	
	
	// overwrite clientside code
	Element.Properties.inputValue = {
	 
		get: function(){
				 switch(this.get('tag')) {
					case 'select':
						vals = this.getSelected().map(function(op){ 
							var v = $pick(op.get('value'),op.get('text')); 
							//return (v=="")?op.get('text'):v;
							return v;
						});
						return this.get('multiple')?vals:vals[0];
					case 'input':
						switch(this.get('type')) {
							case 'checkbox':
								//return this.get('checked')?this.get('value'):false;								
								return this.get('checked') ? true : false;
							case 'radio':
								var checked;
								if (this.get('checked')) return this.get('value');
								$(this.getParent('form')||document.body).getElements('input').each(function(input){
									if (input.get('name') == this.get('name') && input.get('checked')) checked = input.get('value');
								}, this);
								return checked||null;
						}
					case 'input': case 'textarea':
						return this.get('value');
					default:
						return this.get('inputValue');
				 }
		},
	 
		set: function(value){
				switch(this.get('tag')){
					case 'select':
						this.getElements('option').each(function(op){
							var v = $pick(op.get('value'), op.get('text'));
							if (v=="") v = op.get('text');
							//op.set('selected', $splat(value).contains(v));
							op.removeAttribute('selected');
							if ($splat(value).contains(v)) op.set('selected', true);
						});
						break;
					case 'input':
						if (['radio','checkbox'].contains(this.get('type'))) {
							////console.log('setting value: ' + value);
							//this.set('checked', $type(value)=="boolean"?value:$splat(value).contains(this.get('value')));							
							//this.set('checked', $type(value) == 'boolean' ? value : 'checked');
							//console.log('fn for checkboxes needs to be added');
							if (this.get('type') == 'radio') {
								var els = document.body.getElements('input[name='+this.get('name')+']');	
													
								$each(els, function(input) {									
									input.checked = (value == input.get('value')) ? true : false;
								}, this);
							}
							break;
						}
					case 'textarea': case 'input':
						this.set('value', value);
						break;
					default:
						this.set('inputValue', value);
				}
				return this;
		},
			
		erase: function() {
			switch(this.get('tag')) {
				case 'select':
					this.getElements('option').each(function(op) {
						op.erase('selected');
					});
					break;
				case 'input':
					if (['radio','checkbox'].contains(this.get('type'))) {
						this.set('checked', false);
						break;
					}
				case 'input': case 'textarea':
					this.set('value', '');
					break;
				default:
					this.set('inputValue', '');
			}
			return this;
		}
	
	};

	// color
	Color.implement({
		match: function(names) {
			$each(names, function(item, index) {
				hex		= '#' + item[0];		
				rgb 	= hex.hexToRgb(true);
				hsb 	= rgb.rgbToHsb();
				item.push(rgb[0], rgb[1], rgb[2], hsb[0], hsb[1], hsb[2]);
			});
			
			var red = this[0], green = this[1], blue = this[2];
			//var hsb = this.rgbToHsb();
			var hue = this.hsb[0], sat = this.hsb[1], lum = this.hsb[2];
			var s = 0, t = -1; i = -1;
			
			$each(names, function(item, index) {
				if (this == '#' + item[0])
					return ["#" + item[0], item[1], true];	// exact color match found
			
				s =	( Math.pow(red - item[2], 2) + Math.pow(green - item[3], 2) + Math.pow(blue - item[4], 2) +
					(Math.abs(Math.pow(hue - item[5], 2)) + Math.pow(sat - item[6], 2) + Math.abs(Math.pow(lum - item[7], 2))) *2) ;
			 
				if (t < 0 || t > s) {	  	
					t = s;
					i = index;
				}
			});
			
			return (i < 0 ? ['#000000', 'Invalid Color: ' + this.rgbToHex(), false] : ['#' + names[i][0], names[i][1], false]);
		}
	});	
	
// ondomready
window.addEvent('domready', function() {
	var mapMaker = new MapMaker();

});	

/*
function extendGMap() {
	dbug.log('extend GMap');
	GMap2.prototype.setNW = function (NW,z) {
		var proj		= this.getCurrentMapType().getProjection();
		var px 			= proj.fromLatLngToPixel(NW,z);
		var sePx 		= new GPoint(px.x + Math.floor(this.getContainer().clientWidth / 2),px.y + Math.floor(this.getContainer().clientHeight / 2));
		var seLatLng	= proj.fromPixelToLatLng(sePx,z);
		this.setCenter(seLatLng,z);
	}	
}
*/

