Source: Map.js

"use strict";

/* global ol, proj4, jsts */

var netgis = netgis || {};

/**
 * Map Implementation for OpenLayers 3+.
 * @param {type} client
 * @param {type} config
 * @returns {netgis.Map}
 */
netgis.Map = function( /*client,*/ config )
{
	//this.client = client;
	this.config = config;
	
	this.mode = null;
	this.interactions = {};
	this.layers = {};
	
	this.viewHistory = [];
	this.viewIndex = -1;
	this.viewFromHistory = false;
	this.viewHistoryMax = 20;
	this.paddingBuffer = 40;
	
	this.hoverFeature = null;
	this.hoverBounds = null;
	this.selectedFeatures = [];
	this.sketchFeatures = [];
	this.snap = null;
	this.snapFeatures = new ol.Collection();
	this.editEventsSilent = false;
	this.selectMultiple = false;
	this.selectReset = false;
	this.drawError = false;
	
	this.initElements();
	this.initMap( config );
	this.initLayers();
	this.initOverlays();
	this.initInteractions();
	this.initConfig( config );
	
	this.setPadding( 0, 0, 0, 0 );
	this.setMode( netgis.Modes.VIEW );
};

netgis.Map.prototype.initElements = function()
{
	this.container = document.createElement( "div" );
	this.container.setAttribute( "tabindex", -1 );
	this.container.className = "netgis-map";
	
	this.container.addEventListener( "pointerleave", this.onPointerLeave.bind( this ) );
	
	this.container.addEventListener( "click", this.onContainerClick.bind( this ) );
	this.container.addEventListener( "contextmenu", this.onRightClick.bind( this ) );
	this.container.addEventListener( "keydown", this.onKeyDown.bind( this ) );
	this.container.addEventListener( "keyup", this.onKeyUp.bind( this ) );
	
	//this.container.addEventListener( "contextmenu", function( e ) { e.preventDefault(); } );
};

netgis.Map.prototype.initMap = function( config )
{
	var mapconfig = config[ "map" ];
	
	// Projections ( WGS / Lon-Lat Supported By Default )
	if ( typeof proj4 !== "undefined" )
	{
		proj4.defs( config[ "projections" ] );
		proj4.defs( "urn:ogc:def:crs:OGC:1.3:CRS84", proj4.defs( "EPSG:4326" ) );
		ol.proj.proj4.register( proj4 );
	}
	
	// View
	var viewParams =
	{
		projection: mapconfig[ "projection" ],
		center: mapconfig[ "center_lonlat" ] ? ol.proj.fromLonLat( mapconfig[ "center_lonlat" ], mapconfig[ "projection" ] ) : mapconfig[ "center" ],
		minZoom: mapconfig[ "min_zoom" ],
		maxZoom: mapconfig[ "max_zoom" ],
		zoom: mapconfig[ "zoom" ]
	};
	
	this.view = new ol.View
	(
		viewParams
	);
	
	//this.setPadding( 0, 0, 0, 0 );

	// Map
	this.map = new ol.Map
	(
		{
			target: this.container,
			view: this.view,
			pixelRatio: 1.0,
			//moveTolerance: 0,
			moveTolerance: ( mapconfig[ "move_tolerance" ] || mapconfig[ "move_tolerance" ] === 0 ) ? mapconfig[ "move_tolerance" ] : 7,
			controls: []
		}
	);
	
	// Scale Bar
	if ( mapconfig[ "scalebar" ] )
	{
		this.scalebar = new ol.control.ScaleLine( { bar: true } );
		this.map.addControl( this.scalebar );
		
		var scales = mapconfig[ "scales" ];
		
		if ( scales && scales.length > 0 )
		{
			this.scalebarSelect = document.createElement( "select" );
			this.scalebarSelect.addEventListener( "change", this.onScalebarSelectChange.bind( this ) );
			this.scalebar.element.appendChild( this.scalebarSelect );
			
			for ( var i = 0; i < scales.length; i++ )
			{
				var scale = scales[ i ];
				var option = document.createElement( "option" );
				option.innerHTML = "1:" + scale;
				option.setAttribute( "value", scale );
				this.scalebarSelect.appendChild( option );
			}
		}
	}
	
	// Map Events
	this.map.on( "moveend", this.onMapMoveEnd.bind( this ) );
	this.map.on( "pointermove", this.onPointerMove.bind( this ) );
	this.map.on( "click", this.onPointerClick.bind( this ) );
	
	// TODO: unify event init
	
	if ( config[ "map" ][ "extent" ] )
	{
		// Wait Until Map In DOM
		var self = this;
		
		window.setTimeout
		(
			function()
			{
				self.map.updateSize();
				self.view.fit( config[ "map" ][ "extent" ] );
			},
			10
		);
	}
};

netgis.Map.prototype.initLayers = function()
{
	// Measure Layer
	this.measureLayer = new ol.layer.Vector( { source: new ol.source.Vector( { features: [] } ), zIndex: 60000, style: this.styleMeasure.bind( this ) } );
	this.map.addLayer( this.measureLayer );
	
	var toolsConfig =  this.config[ "tools" ];
	
	if ( toolsConfig )
	{
		// Non Edit Layer
		this.nonEditLayer = new ol.layer.Vector
		(
			{
				source: new ol.source.Vector( { features: [] } ),
				zIndex: 50000,
				style: this.styleNonEdit.bind( this ),
				updateWhileAnimating: toolsConfig && toolsConfig[ "interactive_render" ] ? toolsConfig[ "interactive_render" ] : false,
				updateWhileInteracting: toolsConfig && toolsConfig[ "interactive_render" ] ? toolsConfig[ "interactive_render" ] : false
			}
		);

		this.map.addLayer( this.nonEditLayer );

		// Edit Layer
		this.editLayer = new ol.layer.Vector
		(
			{
				source: new ol.source.Vector( { features: [] } ),
				zIndex: 50000,
				style: this.styleEdit.bind( this ),
				updateWhileAnimating: toolsConfig && toolsConfig[ "interactive_render" ] ? toolsConfig[ "interactive_render" ] : false,
				updateWhileInteracting: toolsConfig && toolsConfig[ "interactive_render" ] ? toolsConfig[ "interactive_render" ] : false
			}
		);

		this.map.addLayer( this.editLayer );

		this.editLayer.getSource().on( "addfeature", this.onEditLayerAdd.bind( this ) );
		this.editLayer.getSource().on( "changefeature", this.onEditLayerChange.bind( this ) ); //TODO: fired on feature style change? use only one style function with selected/hover states?
		this.editLayer.getSource().on( "removefeature", this.onEditLayerRemove.bind( this ) );

		// Preview Layer
		this.previewLayer = new ol.layer.Vector( { source: new ol.source.Vector( { features: [] } ), zIndex: 55000, style: this.styleSketch.bind( this ) } );
		this.map.addLayer( this.previewLayer );
		//this.addLayer( netgis.Client.Layers.PREVIEW_LAYER, this.previewLayer );

		// Bounds Layer
		this.boundsLayer = null;

		if ( this.config[ "tools" ][ "bounds" ] )
		{		
			var features = new ol.format.GeoJSON().readFeatures( this.config[ "tools" ][ "bounds" ] );

			var style = null;

			if ( this.config[ "tools" ][ "show_bounds" ] && this.config[ "styles" ][ "bounds" ] )
				style = this.createStyle( this.config[ "styles" ][ "bounds" ] );

			this.boundsLayer = new ol.layer.Vector( { source: new ol.source.Vector( { features: features } ), style: style, zIndex: 60000 } );
			this.map.addLayer( this.boundsLayer );
		}
	}
	
	// Geolocation Layer
	this.geolocLayer = null;
	
	if ( this.config[ "modules" ][ "geolocation" ] === true )
	{
		var cfg = this.config[ "geolocation" ];
		
		var style = 
		[
			new ol.style.Style
			(
				{
					image: new ol.style.Circle
					(
						{
							fill: new ol.style.Fill( { color: "#ffffff" } ),
							radius: 8.0
						}
					)
				}
			),
			new ol.style.Style
			(
				{
					image: new ol.style.Circle
					(
						{
							fill: new ol.style.Fill( { color: cfg[ "marker_color" ] ? cfg[ "marker_color" ] : "#ff0000" } ),
							radius: 5.0
						}
					)
				}
			)
		];
		
		var marker = new ol.Feature( { geometry: new ol.geom.Point( ol.proj.fromLonLat( [ 7.0, 50.0 ], this.view.getProjection() ) ) } );
		marker.setId( "geolocation" );
		
		this.geolocLayer = new ol.layer.Vector( { source: new ol.source.Vector( { features: [ marker ] } ), style: style, zIndex: 66000 } );
		this.map.addLayer( this.geolocLayer );
		
		this.geolocLayer.setVisible( false );
	}
};

netgis.Map.prototype.initOverlays = function()
{
	var popupElement = document.createElement( "div" );
	popupElement.className = "netgis-map-overlay";
	
	this.popupOverlay = new ol.Overlay
	(
		{
			id: "popup",
			element: popupElement,
			positioning: "center-center"
		}
	);
	
	this.map.addOverlay( this.popupOverlay );
};

netgis.Map.prototype.initInteractions = function()
{
	// View
	
	this.interactions[ netgis.Modes.VIEW ] =
	[
		new ol.interaction.DragPan(),
		new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
		new ol.interaction.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.ZOOM_BOX ] =
	[
		//new ol.interaction.DragZoom( { condition: ol.events.condition.always, out: false } ),
		new ol.interaction.DragZoom( { condition: function( e ) { return e.originalEvent.button === 0; }, out: false, className: "netgis-zoom-box" } ),
		new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
		new ol.interaction.MouseWheelZoom()
	];
	
	// Measure
	
	this.interactions[ netgis.Modes.MEASURE_LINE ] =
	[
		new ol.interaction.DragPan(),
		new ol.interaction.Modify( { source: this.measureLayer.getSource(), deleteCondition: ol.events.condition.doubleClick, style: this.styleMeasure.bind( this ) } ),
		new ol.interaction.Draw( { type: "LineString", source: this.measureLayer.getSource(), style: this.styleMeasure.bind( this ) } ),
		new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
		new ol.interaction.PinchZoom(),
		new ol.interaction.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.MEASURE_AREA ] =
	[
		new ol.interaction.DragPan(),
		new ol.interaction.Modify( { source: this.measureLayer.getSource(), deleteCondition: ol.events.condition.doubleClick, style: this.styleMeasure.bind( this ) } ),
		new ol.interaction.Draw( { type: "Polygon", source: this.measureLayer.getSource(), style: this.styleMeasure.bind( this ) /* condition: function( e ) { return ol.events.condition.noModifierKeys( e ) && ol.events.condition.primaryAction( e ); }, /*finishCondition: function( e ) { return ol.events.condition.doubleClick( e ) || ( ol.events.condition.primaryAction( e ) === false ); }*/ } ),
		new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
		new ol.interaction.PinchZoom(),
		new ol.interaction.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.MEASURE_LINE ][ 2 ].on( "drawstart", this.onMeasureLineBegin.bind( this ) );
	this.interactions[ netgis.Modes.MEASURE_AREA ][ 2 ].on( "drawstart", this.onMeasureAreaBegin.bind( this ) );
	
	if ( this.config[ "tools" ] && this.config[ "tools" ][ "editable" ] === true )
	{
		// Draw

		var bounds = this.config[ "tools" ][ "bounds" ] ? true : false;

		this.interactions[ netgis.Modes.DRAW_POINTS ] =
		[
			new ol.interaction.Draw( { type: "Point", source: this.editLayer.getSource(), style: this.styleSketch.bind( this ), geometryFunction: bounds ? this.onDrawPointsUpdateGeom.bind( this ) : undefined } ),
			new ol.interaction.DragPan(),
			new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
			new ol.interaction.MouseWheelZoom()
		];

		this.interactions[ netgis.Modes.DRAW_POINTS ][ 0 ].on( "drawend", this.onDrawBufferEnd.bind( this ) );
		if ( bounds ) this.interactions[ netgis.Modes.DRAW_POINTS ][ 0 ].on( "drawend", this.onDrawPointsEnd.bind( this ) );

		this.interactions[ netgis.Modes.DRAW_LINES ] =
		[
			new ol.interaction.Draw( { type: "LineString", source: this.editLayer.getSource(), style: this.styleSketch.bind( this ), geometryFunction: bounds ? this.onDrawLinesUpdateGeom.bind( this ) : undefined } ),
			new ol.interaction.DragPan(),
			new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
			new ol.interaction.MouseWheelZoom()
		];

		this.interactions[ netgis.Modes.DRAW_LINES ][ 0 ].on( "drawend", this.onDrawBufferEnd.bind( this ) );
		if ( bounds ) this.interactions[ netgis.Modes.DRAW_LINES ][ 0 ].on( "drawend", this.onDrawLinesEnd.bind( this ) );

		this.interactions[ netgis.Modes.DRAW_POLYGONS ] =
		[
			new ol.interaction.Draw( { type: "Polygon", source: this.editLayer.getSource(), style: this.styleSketch.bind( this ), geometryFunction: bounds ? this.onDrawPolygonsUpdateGeom.bind( this ) : undefined } ),
			new ol.interaction.DragPan(),
			new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
			new ol.interaction.MouseWheelZoom()
		];

		if ( bounds ) this.interactions[ netgis.Modes.DRAW_POLYGONS ][ 0 ].on( "drawend", this.onDrawPolygonsEnd.bind( this ) );

		// Edit

		this.interactions[ netgis.Modes.MODIFY_FEATURES ] =
		[
			new ol.interaction.DragPan(),
			new ol.interaction.Modify( { source: this.editLayer.getSource(), deleteCondition: ol.events.condition.doubleClick, style: this.styleModify.bind( this ) } ),
			new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
			new ol.interaction.MouseWheelZoom()
		];

		//this.interactions[ netgis.Modes.MODIFY_FEATURES ][ 0 ].on( "modifyend", this.onModifyFeaturesEnd.bind( this ) );

		this.interactions[ netgis.Modes.DELETE_FEATURES ] =
		[
			//new ol.interaction.Select( { layers: [ this.editLayer ], addCondition: ol.events.condition.pointerMove } ),
			new ol.interaction.DragPan(),
			new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
			new ol.interaction.MouseWheelZoom()
		];

		this.interactions[ netgis.Modes.CUT_FEATURES_DRAW ] =
		[
			new ol.interaction.Draw( { type: "Polygon" /*, source: this.editLayer.getSource()*/, style: this.styleSketch.bind( this ) } ),
			new ol.interaction.DragPan(),
			new ol.interaction.DragPan( { condition: function( e ) { return e.originalEvent.button === 1; } } ),
			new ol.interaction.MouseWheelZoom()
		];

		this.interactions[ netgis.Modes.CUT_FEATURES_DRAW ][ 0 ].on( "drawend", this.onCutFeaturesDrawEnd.bind( this ) );
	}
};

netgis.Map.prototype.initConfig = function( config )
{
	// TODO: move map view config here?
	
	// Map View
	if ( config[ "map" ][ "bbox" ] )
	{
		this.zoomBBox( config[ "map" ][ "bbox" ] );
	}
	
	if ( config[ "map" ][ "zoom" ] )
	{
		this.view.setZoom( config[ "map" ][ "zoom" ] );
	}
	
	// Add Active Layers ( Top To Bottom Order )
	var configLayers = config[ "layers" ];
	
	for ( var i = configLayers.length - 1; i >= 0; i-- )
	{
		var layer = configLayers[ i ];
		if ( layer[ "active" ] === true ) this.addLayer( layer[ "id" ], layer );
	}
};

netgis.Map.prototype.attachTo = function( parent )
{
	parent.appendChild( this.container );
	
	parent.addEventListener( netgis.Events.CLIENT_CONTEXT_RESPONSE, this.onClientContextResponse.bind( this ) );
	parent.addEventListener( netgis.Events.CLIENT_SET_MODE, this.onClientSetMode.bind( this ) );
	parent.addEventListener( netgis.Events.PANEL_TOGGLE, this.onPanelToggle.bind( this ) );
	parent.addEventListener( netgis.Events.PANEL_RESIZE, this.onPanelResize.bind( this ) );
	
	parent.addEventListener( netgis.Events.MAP_EDIT_LAYER_LOADED, this.onEditLayerLoaded.bind( this ) );
	
	parent.addEventListener( netgis.Events.MAP_ZOOM, this.onMapZoom.bind( this ) );
	parent.addEventListener( netgis.Events.MAP_ZOOM_HOME, this.onMapZoomHome.bind( this ) );
	parent.addEventListener( netgis.Events.MAP_ZOOM_LONLAT, this.onMapZoomLonLat.bind( this ) );
	parent.addEventListener( netgis.Events.MAP_ZOOM_SCALE, this.onMapZoomScale.bind( this ) );
	parent.addEventListener( netgis.Events.MAP_ZOOM_LAYER, this.onMapZoomLayer.bind( this ) );
	parent.addEventListener( netgis.Events.MAP_ZOOM_LEVEL, this.onMapZoomLevel.bind( this ) );
	
	parent.addEventListener( netgis.Events.MAP_LAYER_TOGGLE, this.onMapLayerToggle.bind( this ) );
	parent.addEventListener( netgis.Events.MAP_LAYER_TRANSPARENCY, this.onMapLayerTransparency.bind( this ) );
	
	parent.addEventListener( netgis.Events.MAP_SNAP_TOGGLE, this.onMapSnapToggle.bind( this ) );
	
	parent.addEventListener( netgis.Events.MAP_VIEW_PREV, this.onMapViewPrev.bind( this ) );
	parent.addEventListener( netgis.Events.MAP_VIEW_NEXT, this.onMapViewNext.bind( this ) );
	
	parent.addEventListener( netgis.Events.GEOLOCATION_TOGGLE_ACTIVE, this.onGeolocToggleActive.bind( this ) );
	parent.addEventListener( netgis.Events.GEOLOCATION_CHANGE, this.onGeolocChange.bind( this ) );
	
	parent.addEventListener( netgis.Events.MEASURE_CLEAR, this.onMeasureClear.bind( this ) );
	
	parent.addEventListener( netgis.Events.SELECT_MULTI_TOGGLE, this.onSelectMultiToggle.bind( this ) );
	
	parent.addEventListener( netgis.Events.DRAW_BUFFER_TOGGLE, this.onDrawBufferToggle.bind( this ) );
	parent.addEventListener( netgis.Events.DRAW_BUFFER_CHANGE, this.onDrawBufferChange.bind( this ) );
	
	parent.addEventListener( netgis.Events.BUFFER_CHANGE, this.onBufferChange.bind( this ) );
	parent.addEventListener( netgis.Events.BUFFER_ACCEPT, this.onBufferAccept.bind( this ) );
	
	parent.addEventListener( netgis.Events.IMPORT_LAYER_ACCEPT, this.onImportLayerAccept.bind( this ) );
	parent.addEventListener( netgis.Events.IMPORT_LAYER_PREVIEW, this.onImportLayerPreview.bind( this ) );
	parent.addEventListener( netgis.Events.IMPORT_GEOPORTAL_SUBMIT, this.onImportGeoportalSubmit.bind( this ) );
	
	parent.addEventListener( netgis.Events.MAP_COPY_FEATURE_TO_EDIT, this.onCopyFeatureToEdit.bind( this ) );
	
	//parent.addEventListener( netgis.Events.SEARCHPARCEL_FIELDS_RESPONSE, this.onSearchParcelFieldsResponse.bind( this ) );
	parent.addEventListener( netgis.Events.SEARCHPARCEL_ITEM_ENTER, this.onSearchParcelItemEnter.bind( this ) );
	parent.addEventListener( netgis.Events.SEARCHPARCEL_ITEM_LEAVE, this.onSearchParcelItemLeave.bind( this ) );
	parent.addEventListener( netgis.Events.SEARCHPARCEL_ITEM_CLICK, this.onSearchParcelItemClick.bind( this ) );
	parent.addEventListener( netgis.Events.SEARCHPARCEL_ITEM_IMPORT, this.onSearchParcelItemImport.bind( this ) );
	
	parent.addEventListener( netgis.Events.EXPORT_BEGIN, this.onExportBegin.bind( this ) );
	
	parent.addEventListener( netgis.Events.TIMESLIDER_SHOW, this.onTimeSliderShow.bind( this ) );
	parent.addEventListener( netgis.Events.TIMESLIDER_HIDE, this.onTimeSliderHide.bind( this ) );
	parent.addEventListener( netgis.Events.TIMESLIDER_SELECT, this.onTimeSliderSelect.bind( this ) );
};

netgis.Map.prototype.setMode = function( mode )
{
	//console.info( "Map Set Mode:", this.mode, "->", mode );
	
	// Leave
	switch( this.mode )
	{
		case netgis.Modes.VIEW:
		{
			this.container.classList.remove( "netgis-clickable" );
			break;
		}
		
		case netgis.Modes.MODIFY_FEATURES:
		{
			this.editLayer.setStyle( this.styleEdit.bind( this ) );
			break;
		}
		
		case netgis.Modes.DRAW_POINTS:
		case netgis.Modes.DRAW_LINES:
		{
			this.previewLayer.getSource().clear();
			this.container.classList.remove( "netgis-not-allowed" );
			this.container.removeAttribute( "title" );
			break;
		}
		
		case netgis.Modes.DRAW_POLYGONS:
		{
			this.container.classList.remove( "netgis-not-allowed" );
			this.container.removeAttribute( "title" );
			break;
		}
		
		case netgis.Modes.BUFFER_FEATURES:
		{
			this.clearSketchFeatures();
			break;
		}
		
		case netgis.Modes.BUFFER_FEATURES_EDIT:
		{
			this.clearSketchFeatures();
			this.selectedFeatures = [];
			this.redrawVectorLayers();
			break;
		}
		
		case netgis.Modes.BUFFER_FEATURES_DYNAMIC:
		{
			this.clearSketchFeatures();
			this.selectedFeatures = [];
			this.redrawVectorLayers();
			break;
		}
		
		case netgis.Modes.CUT_FEATURES:
		{
			if ( mode !== netgis.Modes.CUT_FEATURES_DRAW )
			{
				this.selectedFeatures = [];
				this.redrawVectorLayers();
			}
			
			break;
		}
		
		case netgis.Modes.CUT_FEATURES_DRAW:
		{
			this.selectedFeatures = [];
			this.redrawVectorLayers();
			break;
		}
	}
	
	this.map.getInteractions().clear();
	
	if ( this.mode ) this.container.classList.remove( "netgis-mode-" + this.mode );
	
	// Enter
	var interactions = this.interactions[ mode ];
	
	if ( ! interactions )
	{
		console.warn( "no interactions found for mode", mode );
		interactions = this.interactions[ netgis.Modes.VIEW ];
	}
	
	for ( var i = 0; i < interactions.length; i++ )
	{
		this.map.addInteraction( interactions[ i ] );
	}
	
	var editable = this.config[ "tools" ] && this.config[ "tools" ][ "editable" ];
	
	switch ( mode )
	{
		case netgis.Modes.DRAW_POINTS:
		case netgis.Modes.DRAW_LINES:
		{
			if ( editable )
			{
				this.setSnapping( this.drawSnapOn );
				this.onDrawBufferToggle( { detail: { on: this.drawBufferOn, radius: this.drawBufferRadius, segments: this.drawBufferSegments } } );
			}
			
			break;
		}
		
		case netgis.Modes.DRAW_POLYGONS:
		{
			if ( editable )
			{
				this.setSnapping( this.drawSnapOn );
			}
			
			break;
		}
		
		case netgis.Modes.MODIFY_FEATURES:
		{
			if ( editable )
			{
				this.setSnapping( this.drawSnapOn );
				this.editLayer.setStyle( this.styleModify.bind( this ) );
			}
			
			break;
		}
	}
	
	this.container.classList.add( "netgis-mode-" + mode );
	
	this.mode = mode;
};

netgis.Map.prototype.addLayer = function( id, params )
{
	//console.info( "Add Layer:", id, params, this.mode );
	
	var layer = this.createLayer( params );
	
	if ( layer )
	{
		layer.set( "id", id );
		
		this.map.addLayer( layer );
		this.layers[ id ] = layer;
		
		if ( params[ "order" ] ) layer.setZIndex( params[ "order" ] );
		if ( params[ "transparency" ] ) layer.setOpacity( 1.0 - params[ "transparency" ] );
		if ( params[ "style" ] ) layer.setStyle( this.createStyle( params[ "style" ] ) );
		if ( params[ "min_zoom" ] ) layer.setMinZoom( params[ "min_zoom" ] - 0.0001 ); // NOTE: subtract small bias because it's exclusive in OL
		if ( params[ "max_zoom" ] ) layer.setMaxZoom( params[ "max_zoom" ] );
		
		if ( layer instanceof ol.layer.Vector ) this.addSnapLayer( layer );
		
		if ( params[ "type" ] === netgis.LayerTypes.WMST )
		{
			netgis.util.invoke( this.container, netgis.Events.TIMESLIDER_SHOW, { layer: id, title: params[ "title" ], url: params[ "url" ], name: params[ "name" ] } );
		}
	}
	
	return layer;
};

netgis.Map.prototype.isLayerQueryable = function( layer )
{
	// TODO: refactor with Info.isLayerQueryable ! static method ?
	
	var queryable = false;
	
	if ( layer[ "query" ] === true )
	{
		// Queryable Config Layers
		queryable = true;
	}
	else if ( layer[ "query" ] !== false )
	{
		// Default Query Behavior For WMS
		switch ( layer[ "type" ] )
		{
			case netgis.LayerTypes.WMS:
			case netgis.LayerTypes.WMST:
			{
				queryable = true;
				break;
			}
		}
	}
	
	//console.error( "Map Layer Queryable:", layer, queryable );
	
	return queryable;
};

netgis.Map.prototype.getQueryableLayers = function( novectors )
{
	var layers = [];
	
	for ( var i = 0; i < this.config[ "layers" ].length; i++ )
	{
		var params = this.config[ "layers" ][ i ];
		var layer = this.layers[ params[ "id" ] ];
		
		if ( ! layer ) continue;
		
		// Exclude Vector Layers
		if ( novectors && ( layer instanceof ol.layer.Vector ) ) continue;
		
		if ( this.isLayerQueryable( params ) ) layers.push( params );
	}
	
	return layers;
};

netgis.Map.prototype.createLayer = function( params )
{
	var layer;
	
	switch ( params[ "type" ] )
	{
		case netgis.LayerTypes.HIDDEN:
		{
			break;
		}
		
		// Raster Layers
		
		case netgis.LayerTypes.TMS:
		case netgis.LayerTypes.XYZ:
		{
			layer = this.createLayerTMS( params[ "url" ], params[ "projection" ], params[ "extent" ], params[ "scales" ] );
			break;
		}
		
		case netgis.LayerTypes.OSM:
		{
			layer = this.createLayerTMS( "https://{a-c}.tile.openstreetmap.de/{z}/{x}/{y}.png" );
			break;
		}
		
		case netgis.LayerTypes.WMTS:
		{
			layer = this.createLayerWMTS( params[ "url" ], params[ "name" ] );
			break;
		}
		
		case netgis.LayerTypes.WMS:
		{
			layer = this.createLayerWMS( params[ "url" ], params[ "name" ], params[ "format" ], params[ "tiled" ], params[ "username" ], params[ "password" ] );
			break;
		}
		
		case netgis.LayerTypes.WMST:
		{
			layer = this.createLayerWMST( params[ "url" ], params[ "name" ], params[ "format" ], params[ "tiled" ], params[ "username" ], params[ "password" ] );
			break;
		}
		
		// Vector Layers
		
		case netgis.LayerTypes.GEOJSON:
		{
			var data = params[ "data" ];
			if ( data && netgis.util.isString( data ) ) data = JSON.parse( data );
			
			layer = this.createLayerGeoJSON( data ? data : params[ "url" ] );
			break;
		}
		
		case netgis.LayerTypes.WFS:
		{
			layer = this.createLayerWFS( params[ "url" ], params[ "name" ], this.view.getProjection().getCode(), params[ "format" ], params[ "username" ], params[ "password" ] );
			break;
		}
		
		case netgis.LayerTypes.VTILES:
		{
			layer = this.createLayerVectorTiles( params[ "url" ], params[ "extent" ], params[ "min_zoom" ], params[ "max_zoom" ] );
			break;
		}
		
		case netgis.LayerTypes.GML:
		{
			layer = this.createLayerGML( params[ "data" ] );
			break;
		}
		
		case netgis.LayerTypes.KML:
		{
			layer = this.createLayerKML( params[ "url" ] );
			break;
		}
		
		case netgis.LayerTypes.GEOPACKAGE:
		{
			layer = this.createLayerGeoPackage( params[ "data" ] );
			break;
		}
		
		case netgis.LayerTypes.SPATIALITE:
		{
			layer = this.createLayerSpatialite( params[ "data" ] );
			break;
		}
		
		case netgis.LayerTypes.SHAPEFILE:
		{
			layer = this.createLayerShapefile( params[ "data" ] );
			break;
		}
		
		case netgis.LayerTypes.WKT:
		{
			layer = this.createLayerWKT( params[ "data" ] );
			break;
		}
		
		default:
		{
			console.error( "unknown layer type", params[ "type" ] );
			break;
		}	
	}
	
	return layer;
};

netgis.Map.prototype.removeLayer = function( id )
{
	//console.info( "Remove Layer:", id );
	
	var layer = this.layers[ id ];
	
	if ( layer instanceof ol.layer.Vector ) this.removeSnapLayer( layer );
	
	for ( var i = 0; i < this.config[ "layers" ].length; i++ )
	{
		var params = this.config[ "layers" ][ i ];
		
		if ( params[ "id" ] === id )
		{			
			// TODO: optimize this (special case for wmst)
			if ( params[ "type" ] === netgis.LayerTypes.WMST )
			{
				netgis.util.invoke( this.container, netgis.Events.TIMESLIDER_HIDE, null );
			}
		}
	}
	
	this.map.removeLayer( layer );
	delete this.layers[ id ];
};

netgis.Map.prototype.setLayerOrder = function( id, order )
{
	var layer = this.layers[ id ];
	layer.setZIndex( order );
};

netgis.Map.prototype.createStyle = function( config )
{
	var radius = config[ "radius" ] ? config[ "radius" ] : 3.0;
	var width = config[ "width" ] ? config[ "width" ] : 1.0;
	var fill = config[ "fill" ] ? config[ "fill" ] : "gray";
	var stroke = config[ "stroke" ] ? config[ "stroke" ] : "black";
	
	var styler = function( feature )
	{
		var style =
		//[
			// Background
			/*new ol.style.Style
			(
				{
					image: new ol.style.Circle( { radius: config[ "radius" ], fill: new ol.style.Fill( { color: config[ "stroke" ] } ) } )
				}
			),*/

			// Foreground
			new ol.style.Style
			(
				{
					image: new ol.style.Circle( { radius: radius - width, fill: new ol.style.Fill( { color: fill } ) } ),
					fill: new ol.style.Fill( { color: fill } ),
					stroke: new ol.style.Stroke( { color: stroke, width: width } )
				}
			);
		//];

		//if ( label ) style.push( label );

		/*if ( labelConfig && labelConfig.length > 0 )
		{
			var str = feature.get( labelConfig );
			if ( ! str ) str = labelConfig;

			//var str = "Label";

			//var label = new ol.style.Text
			var label = new ol.style.Style
			(
				{
					text: new ol.style.Text
					(
						{
							text: [ str, "4mm sans-serif" ],
							font: "4mm Verdana, sans-serif",
							fill: new ol.style.Fill( { color: "rgba( 0, 0, 0, 1.0 )" } ),
							backgroundFill: new ol.style.Fill( { color: "rgba( 255, 255, 255, 0.5 )" } ),
							padding: [ 2, 4, 2, 4 ],
							overflow: true
						}
					)
				}
			);

			//classified.setText( label );
			style.push( label );
		}*/

		return style;
	};
	
	return styler;
};

netgis.Map.prototype.styleMeasure = function( feature )
{
	var geom = feature.getGeometry();
	var cfg = this.config[ "measure" ];
	
	// Line
	var style = new ol.style.Style
	(
		{
			fill: new ol.style.Fill( { color: cfg[ "area_fill" ] } ),
			stroke: new ol.style.Stroke( { color: cfg[ "line_color" ], width: cfg[ "line_width" ], lineDash: cfg[ "line_dash" ] } )
		}
	);
	
	// Label
	if ( geom instanceof ol.geom.Polygon )
	{
		var area = geom.getArea();
		
		style.setText
		(
			new ol.style.Text
			(
				{
					text: [ netgis.util.formatArea( area, true ), "4mm sans-serif" ],
					font: "Arial",
					fill: new ol.style.Fill( { color: cfg[ "text_color" ] } ),
					backgroundFill: new ol.style.Fill( { color: cfg[ "text_back" ] } ),
					padding: [ 2, 4, 2, 4 ],
					overflow: true
				}
			)
		);
	}
	else if ( geom instanceof ol.geom.LineString && ( this.mode === netgis.Modes.MEASURE_LINE || this.mode === netgis.Modes.VIEW ) )
	{
		var len = geom.getLength();
		
		style.setText
		(
			new ol.style.Text
			(
				{
					text: [ netgis.util.formatDistance( len ), "4mm sans-serif" ],
					font: "Arial",
					fill: new ol.style.Fill( { color: cfg[ "text_color" ] } ),
					backgroundFill: new ol.style.Fill( { color: cfg[ "text_back" ] } ),
					padding: [ 2, 4, 2, 4 ],
					overflow: true
				}
			)
		);
	}
	
	// Points
	if ( cfg[ "point_radius" ] && cfg[ "point_radius" ] > 0.0 )
	{
		var points = this.getGeometryPoints( feature );

		/*
		var shadow = new ol.style.Style
		(
			{
				image: new ol.style.Circle( { radius: 5.0, fill: new ol.style.Fill( { color: "rgba( 0, 0, 0, 0.5 )" } ), displacement: [ 0, -1 ] } ),
				geometry: points
			}
		);
		*/

		var outline = new ol.style.Style
		(
			{
				image: new ol.style.Circle( { radius: cfg[ "point_radius" ] * 1.25, fill: new ol.style.Fill( { color: cfg[ "point_stroke" ] } ) } ),
				geometry: points
			}
		);

		var vertex = new ol.style.Style
		(
			{
				image: new ol.style.Circle( { radius: cfg[ "point_radius" ], fill: new ol.style.Fill( { color: cfg[ "point_fill" ] } ) } ),
				geometry: points
			}
		);

		return [ style, outline, vertex ];
	}
	
	return style;
};

netgis.Map.prototype.styleEdit = function( feature )
{
	var configDraw = this.config[ "styles" ][ "draw" ];
	var configSelect = this.config[ "styles" ][ "select" ]; // select ?
	
	var geom = feature.getGeometry();
	
	var selected = ( this.hoverFeature === feature );
	
	if ( this.selectedFeatures.indexOf( feature ) > -1 ) selected = true;
	
	var radius = selected ? configSelect[ "radius" ] : configDraw[ "radius" ];
	var fill = selected ? configSelect[ "fill" ] : configDraw[ "fill" ];
	var stroke = selected ? configSelect[ "stroke" ] : configDraw[ "stroke" ];
	
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: radius, fill: new ol.style.Fill( { color: stroke } ) } ),
			fill: new ol.style.Fill( { color: fill } ),
			stroke: new ol.style.Stroke( { color: stroke, width: configDraw[ "width" ] } )
		}
	);
	
	if ( selected ) style.setZIndex( 1 );
	
	// Text Labels
	if ( geom instanceof ol.geom.Polygon || geom instanceof ol.geom.MultiPolygon )
	{
		var area = geom.getArea();
		
		if ( ! area || area <= 0 ) return style;
		
		// Labels In Viewport
		if ( configDraw[ "viewport_labels" ] === true )
		{
			// NOTE: https://gis.stackexchange.com/questions/320743/openlayers-keep-text-style-label-in-visible-polygon-area
			
			var viewExtent = this.view.calculateExtent( this.map.getSize() );
			var viewGeom = ol.geom.Polygon.fromExtent( viewExtent );
			
			var parser = new jsts.io.OL3Parser();
			
			var a = parser.read( geom );
			var b = parser.read( viewGeom );
			
			var clip = a.intersection( b );
			var clipGeom = parser.write( clip );
			
			style.setGeometry( clipGeom );
		}
		
		// Label String
		var str = "";
		
		// Simple Feature Titles
		str = netgis.util.formatArea( area, true );
		
		var props = feature.getProperties();
		var title = props[ "title" ];
		
		if ( title ) str = title + "\n" + str;
		
		style.setText
		(
			new ol.style.Text
			(
				{
					text: str,
					font: "4mm Arial, sans-serif",
					fill: new ol.style.Fill( { color: stroke } ),
					backgroundFill: new ol.style.Fill( { color: "rgba( 255, 255, 255, 0.5 )" } ),
					padding: [ 2, 4, 2, 4 ]
				}
			)
		);
		
	}
	
	return style;
};

netgis.Map.prototype.styleNonEdit = function( feature )
{
	// TODO: refactor with edit style
	
	var configDraw = this.config[ "styles" ][ "non_edit" ];
	var configSelect = this.config[ "styles" ][ "select" ]; // select ?
	
	var geom = feature.getGeometry();
	
	var selected = ( this.hoverFeature === feature );
	
	if ( this.selectedFeatures.indexOf( feature ) > -1 ) selected = true;
	
	var radius = selected ? configSelect[ "radius" ] : configDraw[ "radius" ];
	var fill = selected ? configSelect[ "fill" ] : configDraw[ "fill" ];
	var stroke = selected ? configSelect[ "stroke" ] : configDraw[ "stroke" ];
	
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: radius, fill: new ol.style.Fill( { color: stroke } ) } ),
			fill: new ol.style.Fill( { color: fill } ),
			stroke: new ol.style.Stroke( { color: stroke, width: configDraw[ "width" ] } )
		}
	);
	
	if ( selected ) style.setZIndex( 1 );
	
	// Text Labels
	if ( geom instanceof ol.geom.Polygon )
	{
		var area = geom.getArea();
		
		if ( ! area || area <= 0 ) return style;
		
		// Labels In Viewport
		if ( configDraw[ "viewport_labels" ] === true )
		{
			// NOTE: https://gis.stackexchange.com/questions/320743/openlayers-keep-text-style-label-in-visible-polygon-area
			
			var viewExtent = this.map.getView().calculateExtent( this.map.getSize() );
			var viewGeom = ol.geom.Polygon.fromExtent( viewExtent );
			
			var parser = new jsts.io.OL3Parser();
			
			var a = parser.read( geom );
			var b = parser.read( viewGeom );
			
			var clip = a.intersection( b );
			var clipGeom = parser.write( clip );
			
			style.setGeometry( clipGeom );
		}
		
		// Label String
		var str = "";
		
		// Simple Feature Titles
		str = netgis.util.formatArea( area, true );
		
		var props = feature.getProperties();
		var title = props[ "title" ];
		
		if ( title ) str = title + "\n" + str;
		
		/*
		// TODO: fully implement label templates
		
		var template = configDraw[ "labels" ];
		
		if ( template )
		{
			str = template;
			str = netgis.util.replace( str, "{area}", netgis.util.formatArea( area, true ) );
			
			var props = feature.getProperties();
			
			for ( var k in props )
			{
				var v = props[ k ];
				
				// TODO: not executed if feature doesn't have key
				if ( ( ! v ) || ( v === null ) ) v = "";
				
				str = netgis.util.replace( str, "{" + k + "}", v );
			}
		}
		else
		{
			str = netgis.util.formatArea( area, true );
		}
		*/
		
		style.setText
		(
			new ol.style.Text
			(
				{
					text: str,
					font: "4mm Arial, sans-serif",
					fill: new ol.style.Fill( { color: stroke } ),
					backgroundFill: new ol.style.Fill( { color: "rgba( 255, 255, 255, 0.5 )" } ),
					padding: [ 2, 4, 2, 4 ]
				}
			)
		);
		
	}
	
	return style;
};

netgis.Map.prototype.styleSketch = function( feature )
{	
	var config = this.config[ "styles" ][ this.drawError ? "error" : "sketch" ];
	
	var geom = feature.getGeometry();
	
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: config[ "radius" ], fill: new ol.style.Fill( { color: config[ "fill" ] } ) } ),
			fill: new ol.style.Fill( { color: config[ "fill" ] } ),
			stroke: new ol.style.Stroke( { color: config[ "stroke" ], width: config[ "width" ] } )
		}
	);
	
	if ( geom instanceof ol.geom.Polygon )
	{
		var area = geom.getArea();
		
		style.setText
		(
			new ol.style.Text
			(
				{
					text: [ netgis.util.formatArea( area, true ), "4mm sans-serif" ],
					font: "Arial",
					fill: new ol.style.Fill( { color: config[ "stroke" ] } ),
					backgroundFill: new ol.style.Fill( { color: "rgba( 255, 255, 255, 0.5 )" } ),
					padding: [ 2, 4, 2, 4 ]
				}
			)
		);
	}
	
	var vertex = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: config[ "radius" ], fill: new ol.style.Fill( { color: config[ "stroke" ] } ) } ),
			geometry: this.getGeometryPoints( feature )
		}
	);
	
	return [ style, vertex ];
};

netgis.Map.prototype.styleModify = function( feature )
{
	var config = this.config[ "styles" ][ "modify" ];
	
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: config[ "radius" ], fill: new ol.style.Fill( { color: config[ "stroke" ] } ) } ),
			fill: new ol.style.Fill( { color: config[ "fill" ] } ),
			stroke: new ol.style.Stroke( { color: config[ "stroke" ], width: config[ "width" ] } )
		}
	);
	
	var vertex = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: config[ "radius" ], fill: new ol.style.Fill( { color: config[ "stroke" ] } ) } ),
			geometry: this.getGeometryPoints( feature )
		}
	);
	
	var geom = feature.getGeometry();
	
	if ( geom instanceof ol.geom.Polygon )
	{
		var area = geom.getArea();
		
		style.setText
		(
			new ol.style.Text
			(
				{
					text: [ netgis.util.formatArea( area, true ), "4mm sans-serif" ],
					font: "Arial",
					fill: new ol.style.Fill( { color: config[ "stroke" ] } ),
					backgroundFill: new ol.style.Fill( { color: "rgba( 255, 255, 255, 0.5 )" } ),
					padding: [ 2, 4, 2, 4 ]
				}
			)
		);
	}
	
	return [ style, vertex ];
};

netgis.Map.prototype.styleHover = function( feature )
{
	/*
	this.hoverStyle = this.createStyle( configSelect[ "fill" ], configSelect[ "stroke" ], configSelect[ "width" ] );
	this.hoverStyle.setZIndex( 1 );
	*/
   
	/*
	var config = this.config[ "styles" ][ "select" ];
	
	console.info( "Style Hover:", feature, config );
   
	var style = this.createStyle( config[ "fill" ], config[ "stroke" ], config[ "width" ] );
	////style.setZIndex( 1 );
	
	return style;
	*/
	
	var config = this.config[ "styles" ][ "select" ];
	
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: config[ "radius" ], fill: new ol.style.Fill( { color: config[ "stroke" ] } ) } ),
			fill: new ol.style.Fill( { color: config[ "fill" ] } ),
			stroke: new ol.style.Stroke( { color: config[ "stroke" ], width: config[ "width" ] } ),
			zIndex: 1
		}
	);
	
	/*var vertex = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: config[ "radius" ], fill: new ol.style.Fill( { color: config[ "stroke" ] } ) } ),
			geometry: this.getGeometryPoints( feature )
		}
	);*/
	
	//return [ style, vertex ];
	
	return style;
};

netgis.Map.prototype.getGeometryPoints = function( feature )
{
	var geometry = feature.getGeometry();

	if ( geometry instanceof ol.geom.LineString )
	{
		return new ol.geom.MultiPoint( geometry.getCoordinates() );
	}
	else if ( geometry instanceof ol.geom.Polygon )
	{
		//return new ol.geom.MultiPoint( geometry.getCoordinates()[ 0 ] );

		var points = [];
		var geomCoords = geometry.getCoordinates();

		for ( var g = 0; g < geomCoords.length; g++ )
		{
			var coords = geomCoords[ g ];

			for ( var c = 0; c < coords.length; c++ )
				points.push( coords[ c ] );
		}

		return new ol.geom.MultiPoint( points );
	}
	else if ( geometry instanceof ol.geom.MultiPolygon )
	{
		var points = [];
		var polys = geometry.getPolygons();
		
		for ( var l = 0; l < polys.length; l++ )
		{
			var geomCoords = polys[ l ].getCoordinates();

			for ( var g = 0; g < geomCoords.length; g++ )
			{
				var coords = geomCoords[ g ];

				for ( var c = 0; c < coords.length; c++ )
					points.push( coords[ c ] );
			}
		}
		
		return new ol.geom.MultiPoint( points );
	}
	else if ( geometry instanceof ol.geom.MultiLineString )
	{
		var points = [];
		var lines = geometry.getPolygons();
		
		for ( var l = 0; l < lines.length; l++ )
		{
			var geomCoords = lines[ l ].getCoordinates();

			for ( var g = 0; g < geomCoords.length; g++ )
			{
				var coords = geomCoords[ g ];

				for ( var c = 0; c < coords.length; c++ )
					points.push( coords[ c ] );
			}
		}
		
		return new ol.geom.MultiPoint( points );
	}
	
	return geometry;
};

netgis.Map.prototype.redrawVectorLayers = function()
{
	// Force Layer Redraw
	this.map.getLayers().forEach
	(		
		function( layer, i, arr )
		{
			if ( layer instanceof ol.layer.Vector || layer instanceof ol.layer.VectorTile )
			{
				layer.setStyle( layer.getStyle() );
			}
		}
	);
};

netgis.Map.prototype.setSnapping = function( on )
{
	var config = this.config[ "tools" ][ "snapping" ];
	
	if ( on )
	{
		this.snap = new ol.interaction.Snap( { features: this.snapFeatures, pixelTolerance: config[ "tolerance" ] ? config[ "tolerance" ] : 10 } );
		this.map.addInteraction( this.snap );

		this.snapFeatures.changed();
	}
	else
	{
		if ( this.snap )
		{
			this.map.removeInteraction( this.snap );
			this.snap = null;
		}
	}
	
	this.drawSnapOn = on;
};

netgis.Map.prototype.setDrawTrace = function( on )
{
	
};

netgis.Map.prototype.addSnapLayer = function( vectorLayer )
{	
	var layerFeatures = vectorLayer.getSource().getFeatures();
			
	for ( var j = 0; j < layerFeatures.length; j++ )
	{
		this.snapFeatures.push( layerFeatures[ j ] );
	}
};

netgis.Map.prototype.removeSnapLayer = function( vectorLayer )
{
	var layerFeatures = vectorLayer.getSource().getFeatures();
			
	for ( var j = 0; j < layerFeatures.length; j++ )
	{
		this.snapFeatures.remove( layerFeatures[ j ] );
	}
};

netgis.Map.prototype.setDrawBuffer = function( on, radius, segments )
{
	//console.info( "DRAW BUFFER:", on, radius, segments );
	
	if ( on )
	{
		var feature = this.createBufferFeature( new ol.geom.Point( this.view.getCenter() ), radius, segments );
		this.previewLayer.getSource().addFeature( feature );
		
		this.drawBufferRadius = radius;
		this.drawBufferSegments = segments;
	}
	else
	{
		this.previewLayer.getSource().clear();
	}
	
	this.drawBufferOn = on;
};

netgis.Map.prototype.createLayerTMS = function( url, projection, extent, scales )
{
	var layer;
	
	if ( projection && extent && scales )
	{
		// Custom Tile Grid
		var resolutions = [];
		scales = ( scales === "map" ) ? this.config[ "map" ][ "scales" ] : scales;
		extent = ( extent === "map" ) ? this.config[ "map" ][ "extent" ] : extent;

		for ( var s = 0; s < scales.length; s++ )
			resolutions.unshift( this.getResolutionFromScale( scales[ s ] ) );
		
		var source = new ol.source.TileImage
		(
			{
				crossOrigin:	null,
				projection:		this.view.getProjection(),
				tileGrid:		new ol.tilegrid.TileGrid
				(
					{
						extent: extent,
						origin: [ extent[ 0 ], extent[ 1 ] ],
						resolutions: resolutions
					}
				),
				tileUrlFunction: function( zxy )
				{
					if ( zxy === null ) return undefined;
					
					var tileURL = url;
					tileURL = netgis.util.replace( tileURL, "{z}", zxy[ 0 ] );
					tileURL = netgis.util.replace( tileURL, "{x}", zxy[ 1 ] );
					tileURL = netgis.util.replace( tileURL, "{y}", zxy[ 2 ] );
					tileURL = netgis.util.replace( tileURL, "{-y}", -zxy[ 2 ] - 1 );

					return tileURL;
				}
			}
		);

		layer = new ol.layer.Tile
		(
			{
				source:	source
			}
		);
	}
	else
	{
		// Default Tile Grid
		layer = new ol.layer.Tile
		(
			{
				source: new ol.source.XYZ
				(
					{
						url: url,
						crossOrigin: "anonymous"
					}
				)
			}
		);
	}
	
	return layer;
};

netgis.Map.prototype.createLayerWMS = function( url, layerName, format, tiled, user, password )
{
	var params =
	{
		url: url,
		params:
		{
			"LAYERS":		layerName,
			"FORMAT":		format ? format : "image/png",
			"TRANSPARENT":	"true"
			//"VERSION":		"1.1.1"
		},
		
		// TODO: how to pass more custom WMS params from config ? json object ?
		
		/*serverType: "mapserver",
		crossOrigin: "anonymous",*/ // TODO: causes cors errors after requests, why?
		hidpi: false
		//ratio: 3.0
	};

	// User Auth
	if ( user && password )
	{
		params.imageLoadFunction = function( image, src )
		{
			var request = new XMLHttpRequest();
			request.open( "GET", src );
			request.setRequestHeader( "Authorization", "Basic " + window.btoa( user + ":" + password ) );

			request.onload = function()
			{
				image.getImage().src = src;
			};

			request.send();
		};
	}
	
	var source;
	var layer;

	if ( tiled )
	{
		source = new ol.source.TileWMS( params );
		layer = new ol.layer.Tile( { source: source } );
	}
	else
	{
		source = new ol.source.ImageWMS( params );
		layer = new ol.layer.Image( { source: source } );
	}
	
	return layer;
};

netgis.Map.prototype.createLayerWMST = function( url, layerName, format, tiled, user, password )
{	
	var params =
	{
		url: url,
		params:
		{
			"LAYERS":		layerName,
			"FORMAT":		format ? format : "image/png",
			"TRANSPARENT":	"true",
			"VERSION":		"1.1.1"
		},
		/*serverType: "mapserver",
		crossOrigin: "anonymous",*/ // TODO: causes cors errors after requests, why?
		hidpi: false
		//ratio: 3.0
	};

	// User Auth
	if ( user && password )
	{
		params.imageLoadFunction = function( image, src )
		{
			var request = new XMLHttpRequest();
			request.open( "GET", src );
			request.setRequestHeader( "Authorization", "Basic " + window.btoa( user + ":" + password ) );

			request.onload = function()
			{
				image.getImage().src = src;
			};

			request.send();
		};
	}
	
	var source;
	var layer;

	if ( tiled )
	{
		source = new ol.source.TileWMS( params );
		layer = new ol.layer.Tile( { source: source } );
	}
	else
	{
		source = new ol.source.ImageWMS( params );
		layer = new ol.layer.Image( { source: source } );
	}
	
	return layer;
};

netgis.Map.prototype.createLayerWMTS = function( url )
{
	//console.info( "WMTS:", url );
	
	var resolutions = [];
	var scales = this.client.config[ "map" ][ "scales" ]; //netgis.config.MAP_SCALES;
	var extent = this.client.config[ "map" ][ "extent" ];

	for ( var s = 0; s < scales.length; s++ )
		resolutions.unshift( this.getResolutionFromScale( scales[ s ] ) );

	var source = new ol.source.TileImage
	(
		{
			crossOrigin:	null,
			projection:		this.view.getProjection(),
			tileGrid:		new ol.tilegrid.TileGrid
			(
				{
					extent: extent,
					origin: [ extent[ 0 ], extent[ 1 ] ],
					resolutions: resolutions
				}
			),
			tileUrlFunction: function( zxy )
			{
				if ( zxy === null ) return undefined;
				
				var tileURL = url;
				tileURL = netgis.util.replace( tileURL, "{z}", zxy[ 0 ] );
				tileURL = netgis.util.replace( tileURL, "{x}", zxy[ 1 ] );
				tileURL = netgis.util.replace( tileURL, "{y}", zxy[ 2 ] );
				tileURL = netgis.util.replace( tileURL, "{-y}", -zxy[ 2 ] );
				
				return tileURL;
			}
		}
	);

	var layer = new ol.layer.Tile
	(
		{
			source:	source
		}
	);
	
	return layer;
};

netgis.Map.prototype.createLayerWMTS_01 = function( url, layerName )
{
	var projection = this.view.getProjection(); //ol.proj.get('EPSG:25832'); //3857 //4326 //900913 EPSG:25832
	var projectionExtent = projection.getExtent();
	var size = ol.extent.getWidth( projectionExtent ) / 256;
	var resolutions = new Array( 14 );
	var matrixIds = new Array( 14 );
	for ( var z = 0; z < 14; ++z )
	{
		// generate resolutions and matrixIds arrays for this WMTS
		resolutions[ z ] = size / Math.pow( 2, z );
		matrixIds[ z ] = z;
	}

	source = new ol.source.WMTS
	(
		{
			url: url,
			params:
			{
				"LAYER":		layerName,
				"FORMAT":		"image/png",
				"TRANSPARENT":	"true",
				"VERSION":		"1.1.1"
			},
			layer: layerName,
			//format: 'image/png',
			format: "image/jpeg",
			matrixSet: "UTM32", //'g',
			tileGrid: new ol.tilegrid.WMTS
			(
				{
					origin: ol.extent.getTopLeft( projectionExtent ),
					resolutions: resolutions,
					matrixIds: matrixIds
				}
			)
		}
	);
};

netgis.Map.prototype.createLayerGeoJSON = function( dataOrURL )
{
	if ( netgis.util.isObject( dataOrURL ) )
	{
		// Direct Object Import
		var format = new ol.format.GeoJSON();
		var projection = format.readProjection( dataOrURL );
		var features = format.readFeatures( dataOrURL, { featureProjection: this.view.getProjection() } );

		// NOTE: proj4.defs[ "EPSG:4326" ]
		// NOTE: netgis.util.foreach( proj4.defs, function( k,v ) { console.info( "DEF:", k, v ); } )

		var projcode = projection.getCode();

		switch ( projcode )
		{
			case "EPSG:3857":
			case "EPSG:4326":
			case this.view.getProjection().getCode():
			{
				// Projection OK
				break;
			}

			default:
			{
				// Projection Not Supported
				console.warn( "unsupported import projection '" + projcode + "'" );
				break;
			}
		}	

		////this.addImportedFeatures( features );

		var layer = new ol.layer.Vector
		(
			{
				source: new ol.source.Vector( { features: features /*, projection: this.view.getProjection().getCode()*/ } )
				//style: this.styleParcel.bind( this ),
				//zIndex: this.editLayerID + 20
			}
		);

		return layer;
	}
	else if ( netgis.util.isString( dataOrURL ) )
	{
		// Request From URL
		var layer = new ol.layer.Vector
		(
			{
				source: new ol.source.Vector( { features: [] } )
			}
		);

		var self = this;
		
		netgis.util.request
		(
			dataOrURL,
			function( response )
			{
				var json = JSON.parse( response );
				var responseLayer = self.createLayerGeoJSON( json );
				layer.getSource().addFeatures( responseLayer.getSource().getFeatures() );
			}
		);

		return layer;
	}
};

netgis.Map.prototype.createLayerGML = function( data )
{	
	//NOTE: https://stackoverflow.com/questions/35935184/opening-qgis-exported-gml-in-openlayers-3
	//NOTE: https://github.com/openlayers/openlayers/issues/5023
	
	console.warn( "GML support is experimental!" );

	var features = [];
	
	var parser = new DOMParser();
	var xml = parser.parseFromString( data, "text/xml" );
	
	// Features
	var featureMembers = xml.getElementsByTagName( "gml:featureMember" );
	
	for ( var f = 0; f < featureMembers.length; f++ )
	{
		var props = {};
		
		var node = featureMembers[ f ];
		var child = node.children[ 0 ];
		
		// Attributes
		for ( var a = 0; a < child.attributes.length; a++ )
		{
			var attribute = child.attributes[ a ];
			props[ attribute.nodeName ] = attribute.nodeValue;
		}
		
		for ( var c = 0; c < child.children.length; c++ )
		{
			var childNode = child.children[ c ];
			
			if ( childNode.nodeName === "ogr:geometryProperty" ) continue;
			
			var parts = childNode.nodeName.split( ":" );
			var k = parts[ parts.length - 1 ];
			var v = childNode.innerHTML;
			
			props[ k ] = v;
		}
		
		// Geometry
		var geomprop = child.getElementsByTagName( "ogr:geometryProperty" )[ 0 ];
		
		//for ( var g = 0; g < geomprop.children.length; g++ )
		{
			var geom = geomprop.children[ 0 ];
			var proj = geom.getAttribute( "srsName" );
			
			if ( proj && proj !== "EPSG:4326" && proj !== this.projection )
				console.warn( "unsupported import projection:", proj );
			
			switch ( geom.nodeName )
			{
				case "gml:Polygon":
				{
					props[ "geometry" ] = this.gmlParsePolygon( geom, proj );
					break;
				}
				
				case "gml:MultiPolygon":
				{
					props[ "geometry" ] = this.gmlParseMultiPolygon( geom, proj );
					break;
				}
			}
		}
		
		var feature = new ol.Feature( props );
		features.push( feature );
	}
	
	////this.addImportedFeatures( features );
	
	var layer = new ol.layer.Vector
	(
		{
			source: new ol.source.Vector( { features: features } )
		}
	);
	
	return layer;
};

netgis.Map.prototype.gmlParsePolygon = function( node, proj )
{
	var rings = [];
	
	var linearRings = node.getElementsByTagName( "gml:LinearRing" );
	
	for ( var r = 0; r < linearRings.length; r++ )
	{
		var ring = linearRings[ r ];
		var coords = ring.getElementsByTagName( "gml:coordinates" )[ 0 ].innerHTML;
		rings.push( this.gmlParseCoordinates( coords, proj ) );
	}
	
	return new ol.geom.Polygon( rings );
};

netgis.Map.prototype.gmlParseMultiPolygon = function( node, proj )
{
	var polygons = [];
					
	var polygonMembers = node.getElementsByTagName( "gml:polygonMember" );

	for ( var p = 0; p < polygonMembers.length; p++ )
	{
		var polygonMember = polygonMembers[ p ];
		var polygonNode = polygonMember.getElementsByTagName( "gml:Polygon" )[ 0 ];
		polygons.push( this.gmlParsePolygon( polygonNode, proj ) );
	}
	
	return new ol.geom.MultiPolygon( polygons );
};

netgis.Map.prototype.gmlParseCoordinates = function( s, proj )
{
	var coords = s.split( " " );
						
	for ( var c = 0; c < coords.length; c++ )
	{
		// Split
		coords[ c ] = coords[ c ].split( "," );

		// Parse
		for ( var xy = 0; xy < coords[ c ].length; xy++ )
		{
			coords[ c ][ xy ] = Number.parseFloat( coords[ c ][ xy ] );
		}
		
		// Transform
		if ( proj ) coords[ c ] = ol.proj.transform( coords[ c ], proj, this.view.getProjection() );
	}
	
	return coords;
};

netgis.Map.prototype.createLayerGeoPackage = function( data )
{
	var layer = new ol.layer.Vector
	(
		{
			source: new ol.source.Vector( { features: [] } )
		}
	);
	
	var self = this;
	var arr = new Uint8Array( data );
	
	window.GeoPackage.setSqljsWasmLocateFile( function( file ) { return self.config[ "import" ][ "geopackage_lib" ] + file; } );
	
	window.GeoPackage.GeoPackageAPI.open( arr ).then
	(
		function( geoPackage )
		{
			var features = [];
			var format = new ol.format.GeoJSON();
			var tables = geoPackage.getFeatureTables();

			for ( var t = 0; t < tables.length; t++ )
			{
				var table = tables[ t ];
				var rows = geoPackage.queryForGeoJSONFeaturesInTable( table );

				for ( var r = 0; r < rows.length; r++ )
				{
					var row = rows[ r ];
					var geom = format.readGeometry( row.geometry, { featureProjection: self.view.getProjection() } );
					var feature = new ol.Feature( { geometry: geom } );
					features.push( feature );
				}
			}

			////self.addImportedFeatures( features );
			layer.getSource().addFeatures( features );
		}
	);
	
	return layer;
};

netgis.Map.prototype.createLayerSpatialite = function( data )
{
	var layer = new ol.layer.Vector
	(
		{
			source: new ol.source.Vector( { features: [] } )
		}
	);
	
	var self = this;
	
	window.initSqlJs().then
	(
		function( SQL )
		{
			var features = [];
			
			var arr = new Uint8Array( data );
			var db = new SQL.Database( arr );
			
			// Tables
			var results = db.exec
			(
				"SELECT name FROM sqlite_schema WHERE type = 'table' \n\
					AND name NOT LIKE 'sqlite_%' \n\
					AND name NOT LIKE 'sql_%' \n\
					AND name NOT LIKE 'idx_%' \n\
					AND name NOT LIKE 'spatial_ref_sys%' \n\
					AND name NOT LIKE 'spatialite_%' \n\
					AND name NOT LIKE 'geometry_columns%' \n\
					AND name NOT LIKE 'views_%' \n\
					AND name NOT LIKE 'virts_%' \n\
					AND name NOT LIKE 'SpatialIndex' \n\
					AND name NOT LIKE 'KNN%' \n\
					AND name NOT LIKE 'ElementaryGeometries' \n\
				;" );
			
			var tables = results[ 0 ].values;
			
			for ( var t = 0; t < tables.length; t++ )
			{
				var table = tables[ t ][ 0 ];

				results = db.exec( "SELECT * FROM " + table );
				var result = results[ 0 ];

				// Columns
				var geomcol = null;

				for ( var c = 0; c < result.columns.length; c++ )
				{
					if ( result.columns[ c ].toLowerCase() === "geometry" ) { geomcol = c; break; }
					if ( result.columns[ c ].toLowerCase() === "geom" ) { geomcol = c; break; }
				}
				
				if ( geomcol === null ) continue;

				// Rows
				var rows = result.values;

				for ( var r = 0; r < rows.length; r++ )
				{
					var row = rows[ r ];
					
					// Convert WKB
					var input = row[ geomcol ];
					var output = new Uint8Array( input.length - 43 - 1 + 5 );

					// Byte Order
					output[ 0 ] = input[ 1 ];

					// Type
					output[ 1 ] = input[ 39 ];
					output[ 2 ] = input[ 40 ];
					output[ 3 ] = input[ 41 ];
					output[ 4 ] = input[ 42 ];

					// Geometry
					var geomlen = input.length - 43 - 1;

					for ( var i = 0; i < geomlen; i++ )
					{
						output[ 5 + i ] = input[ 43 + i ];
					}

					var wkb = new ol.format.WKB();
					var geom = wkb.readGeometry( output, { featureProjection: self.view.getProjection() } );

					features.push( new ol.Feature( { geometry: geom } ) );
				}
			}
			
			////self.addImportedFeatures( features );
			
			layer.getSource().addFeatures( features );
		}
	);
	
	return layer;
};

netgis.Map.prototype.createLayerShapefile = function( data )
{
	var layer = new ol.layer.Vector
	(
		{
			source: new ol.source.Vector( { features: [] } )
		}
	);
	
	var self = this;
	
	shp( data ).then
	(
		function( geojson )
		{			
			//var format = new ol.format.GeoJSON( { dataProjection: "EPSG:4326", featureProjection: "EPSG:3857" } );
			var format = new ol.format.GeoJSON();
			var projection = format.readProjection( geojson );
			var features = format.readFeatures( geojson, { featureProjection: self.view.getProjection() } );
			
			layer.getSource().addFeatures( features );
			
			////self.addImportedFeatures( features );
		}
	);
	
	return layer;
};

netgis.Map.prototype.createLayerWKT = function( data )
{
	var format = new ol.format.WKT();
	var features = [];
	
	// TODO: check if data is array or single wkt string
	
	for ( var i = 0; i < data.length; i++ )
	{
		var item = data[ i ];
		var geom = format.readGeometry( item[ "geometry" ] );
		
		var props = item[ "properties" ];
		props[ "geometry" ] = geom;
		props[ "wkt" ] = item[ "geometry" ];
		
		var feature = new ol.Feature( props );
		feature.setId( item[ "id" ] );
		features.push( feature );
	}
	
	var layer = new ol.layer.Vector( { source: new ol.source.Vector( { features: features } ) } );
	
	return layer;
};

netgis.Map.prototype.createLayerWFS = function( url, typeName, projection, format, user, password )
{
	if ( url[ url.length - 1 ] !== "?" ) url = url + "?";
	
	url = url
			+ "service=WFS"
			+ "&version=1.1.0"
			+ "&request=GetFeature";

	// TODO: always get projection from map view ?
	
	if ( ! projection )
		projection = this.view.getProjection().getCode();
	
	if ( ! format )
		format = "application/json";
	else
		format = netgis.util.replace( format, " ", "+" ); // TODO: encode uri component ?

	var source = new ol.source.Vector
	(
		{
			format: new ol.format.GeoJSON(),
			strategy: ol.loadingstrategy.bbox,
			
			loader: function( extent, resolution, proj, success, failure )
			{
				//proj = proj.getCode();
				
				var requestURL = url
					+ "&typename=" + typeName
					+ "&srsname=" + projection
					+ "&bbox=" + extent.join( "," ) + "," + projection
					+ "&outputFormat=" + format;
				
				var request = new XMLHttpRequest();
				request.open( "GET", requestURL );
				
				if ( user && password )
				{
					request.setRequestHeader( "Authorization", "Basic " + window.btoa( user + ":" + password ) );
				}
				
				request.onerror = function()
				{
					console.error( "WFS request error" );
					failure();
				};
				
				request.onload = function()
				{
					if ( request.status === 200 )
					{
						source.clear();
						
						var features = source.getFormat().readFeatures( request.responseText );
						source.addFeatures( features );
						success( features );
					}
					else
					{
						console.error( "WFS request status", request.status );
						failure();
					}
				};
				
				request.send();
			}
		}
	);

	var layer = new ol.layer.Vector
	(
		{
			source: source
		}
	);

	// TODO: properly handle snap vector sources
	var self = this;
	source.on( "featuresloadstart", function( e ) { self.removeSnapLayer( layer ); } );
	source.on( "featuresloadend", function( e ) { window.setTimeout( function() { self.addSnapLayer( layer ); }, 10 ); } );
	//source.on( "featuresloaderror", function( e ) { console.info( "Layer Error:", e ); } );
	
	return layer;
};

netgis.Map.prototype.createLayerVectorTiles = function( url, extent, minZoom, maxZoom )
{
	// NOTE: https://github.com/openlayers/openlayers/issues/13592 (vector tiles layer extent)
	// NOTE: https://stackoverflow.com/questions/44907234/how-to-set-an-extent-to-a-vectortile-layer-in-openlayers-4-2-0
	
	var layer = new ol.layer.VectorTile
	(
		{
			extent: extent,
			source: new ol.source.VectorTile
			(
				{
					format: new ol.format.MVT(),
					overlaps: true,
					url: url,
					//extent: extent, // TODO: crashes!
					minZoom: minZoom,
					maxZoom: maxZoom
				}
			)
		}
	);
	
	return layer;
};

netgis.Map.prototype.createLayerKML = function( url )
{
	var layer = new ol.layer.Vector( { source: new ol.source.Vector( { features: [] } ) } );
	var self = this;
	
	// TODO: optional parse kml data string instead of url
	
	netgis.util.request
	(
		url,
		function( data )
		{			
			var format = new ol.format.KML();
			var features = format.readFeatures( data, { featureProjection: self.view.getProjection() } );
			
			for ( var i = 0; i < features.length; i++ )
			{
				var feature = features[ i ];
				var props = feature.getProperties();
				
				var styleprops = { fill: "rgba( 127, 127, 127, 0.5 )", stroke: "rgba( 127, 127, 127, 1.0 )", radius: 5.0, width: 3.0 };
				
				for ( var key in props )
				{
					var val = props[ key ];
					
					switch ( key )
					{
						case "fill": { styleprops[ "fill" ] = val; break; }
						case "fill-opacity": { styleprops[ "fill-opacity" ] = val; break; }
						case "stroke": { styleprops[ "stroke" ] = val; break; }
						case "stroke-opacity": { styleprops[ "stroke-opacity" ] = val; break; }
						case "stroke-width": { styleprops[ "width" ] = val; break; }
					}
				}
				
				if ( styleprops[ "fill-opacity" ] )
				{
					var color = netgis.util.hexToRGB( styleprops[ "fill" ] );
					color = "rgba(" + color.join( "," ) + "," + styleprops[ "fill-opacity" ] + ")";
					styleprops[ "fill" ] = color;
				}
				
				if ( styleprops[ "stroke-opacity" ] )
				{
					var color = netgis.util.hexToRGB( styleprops[ "stroke" ] );
					color = "rgba(" + color.join( "," ) + "," + styleprops[ "stroke-opacity" ] + ")";
					styleprops[ "stroke" ] = color;
				}
				
				var style = new ol.style.Style
				(
					{
						image: new ol.style.Circle( { radius: styleprops[ "radius" ], fill: new ol.style.Fill( { color: styleprops[ "stroke" ] } ) } ),
						fill: new ol.style.Fill( { color: styleprops[ "fill" ] } ),
						stroke: new ol.style.Stroke( { color: styleprops[ "stroke" ], width: styleprops[ "width" ] } )
					}
				);
		
				feature.setStyle( style );
			}
			
			layer.getSource().addFeatures( features );
		}
	);
	
	return layer;
};

netgis.Map.prototype.createBufferFeature = function( srcgeom, radius, segments )
{
	var geom = this.createBufferGeometry( srcgeom, radius, segments );
	var feature = new ol.Feature( { geometry: geom } );
	
	return feature;
};

netgis.Map.prototype.createBufferGeometry = function( srcgeom, radius, segments )
{
	var parser = new jsts.io.OL3Parser();
		
	var a = parser.read( srcgeom );
	var b = a.buffer( radius, segments );
	
	if ( this.boundsLayer )
	{
		// Clip Buffer Preview Against Bounds
		var bounds = this.boundsLayer.getSource().getFeatures();
		
		for ( var i = 0; i < bounds.length; i++ )
		{
			var bound = parser.read( bounds[ i ].getGeometry() );
			
			if ( ! b.intersects( bound ) ) continue;
			
			b = b.intersection( bound );
		}
	}
	
	var geom = parser.write( b );
	
	return geom;
};

netgis.Map.prototype.splitMultiPolygons = function( layer )
{
	//TODO: split only selected feature ( parameter )
	
	var source = layer.getSource();
	var features = source.getFeatures();

	var removeFeatures = [];
	var newFeatures = [];

	// Find Multi Features
	for ( var i = 0; i < features.length; i++ )
	{
		var feature = features[ i ];
		var geom = feature.getGeometry();

		if ( geom instanceof ol.geom.MultiPolygon )
		{
			var polygons = geom.getPolygons();

			// Create Single Features
			for ( var j = 0; j < polygons.length; j++ )
			{
				var polygon = polygons[ j ];
				var newFeature = new ol.Feature( { geometry: polygon } );
				newFeatures.push( newFeature );
			}

			removeFeatures.push( feature );
		}
	}

	// Remove Multi Features
	for ( var i = 0; i < removeFeatures.length; i++ )
	{
		source.removeFeature( removeFeatures[ i ] );
	}

	// Add Single Features
	source.addFeatures( newFeatures );
};

netgis.Map.prototype.clearSketchFeatures = function()
{
	var source = this.editLayer.getSource();
	
	// Clear Sketch Features
	for ( var f = 0; f < this.sketchFeatures.length; f++ )
	{
		source.removeFeature( this.sketchFeatures[ f ] );
	}
	
	this.sketchFeatures = [];
};

netgis.Map.prototype.updateDrawBufferPreview = function()
{
	if ( this.config[ "tools" ][ "editable" ] === false ) return;
	
	var draw = this.interactions[ this.mode ][ 0 ];
	var overlays = draw.getOverlay().getSource().getFeatures();
	if ( overlays.length < 1 ) return;
	
	var preview = this.previewLayer.getSource().getFeatures()[ 0 ];
	if ( ! preview ) return;
	
	var geom = overlays[ 0 ].getGeometry();
	var buffer = this.createBufferGeometry( geom, this.drawBufferRadius, this.drawBufferSegments );
	preview.setGeometry( buffer );
};

netgis.Map.prototype.isPointInsideLayer = function( layer, coords )
{
	var features = layer.getSource().getFeatures();
	
	for ( var i = 0; i < features.length; i++ )
	{
		var geom = features[ i ].getGeometry();
		
		if ( geom.intersectsCoordinate( coords ) ) return true;
	}
	
	return false;
};

netgis.Map.prototype.isGeomInsideLayer = function( layer, geom )
{
	var coords = geom.getCoordinates();
	
	if ( geom instanceof ol.geom.LineString )
	{
		if ( coords.length < 2 ) return false;
	}
	else if ( geom instanceof ol.geom.Polygon )
	{
		coords = coords[ 0 ];
		if ( coords.length < 4 ) return false;
		if ( geom.getArea() <= 0 ) return false;
	}
	
	var parser = new jsts.io.OL3Parser();
	var a = parser.read( geom );
	
	var features = layer.getSource().getFeatures();
	
	for ( var i = 0; i < features.length; i++ )
	{
		var other = features[ i ].getGeometry();
		var b = parser.read( other );
		
		if ( b.contains( a ) ) return true;
	}
	
	return false;
};

netgis.Map.prototype.getScaleFromResolution = function( res )
{
	var scale = 39.3701 * 72 * res;
	scale = Math.round( scale );

	return scale;
};

netgis.Map.prototype.getResolutionFromScale = function( scale )
{
	var mpu = ol.proj.Units.METERS_PER_UNIT[ this.view.getProjection().getUnits() ];
	var ipu = mpu * 39.3701; // inches per unit = 39.3701
	var dpi = 72;

	var res = 1 / ( this.normalizeScale( scale ) * ipu * dpi );

	return res;
};

netgis.Map.prototype.normalizeScale = function( scale )
{
	return 1 < scale ? 1 / scale : scale;
};

netgis.Map.prototype.updateEditOutput = function()
{
	var features = this.editLayer.getSource().getFeatures();
	
	//var proj = this.projection;
	var proj = this.view.getProjection().getCode();
	var format = new ol.format.GeoJSON();
	var output = format.writeFeaturesObject( features, { dataProjection: proj, featureProjection: proj } );
	
	// Projection
	output[ "crs" ] =
	{
		"type": "name",
		"properties": { "name": "urn:ogc:def:crs:" + proj.replace( ":", "::" ) }
	};
	
	// Total Area
	var area = 0.0;
	
	for ( var i = 0; i < features.length; i++ )
	{
		var geom = features[ i ].getGeometry();
		if ( geom instanceof ol.geom.Polygon ) area += geom.getArea();
	}
	
	output[ "area" ] = area;
	
	//console.info( "Update Output:", output );
	
	//if ( ! this.editEventsSilent )
	{
		////this.client.invoke( netgis.Events.EDIT_FEATURES_CHANGE, output );
		////netgis.util.invoke( this.container, netgis.Events.EDIT_FEATURES_CHANGE, output );
		netgis.util.invoke( this.container, netgis.Events.MAP_EDIT_LAYER_CHANGE, { geojson: output } );
	}
};

netgis.Map.prototype.updateSnapFeatures = function()
{
	this.snapFeatures.clear();
	
	var self = this;
	
	this.map.getLayers().forEach
	(
		function( layer, i, arr )
		{
			var id = layer.get( "id" );
			
			if ( id === netgis.Client.Layers.PARCEL_DISTRICTS ) return;
			if ( id === netgis.Client.Layers.PARCEL_FIELDS ) return;
			if ( id === netgis.Client.Layers.PARCEL_FEATURES ) return;
			
			if ( layer instanceof ol.layer.Vector )
				self.addSnapLayer( layer );
		}
	);
};

netgis.Map.prototype.zoom = function( delta )
{
	this.view.animate( { zoom: this.view.getZoom() + delta, duration: 200 } );
};

netgis.Map.prototype.zoomLevel = function( z )
{
	this.view.animate( { zoom: z, center: this.view.getCenter(), duration: 300 } );
};

netgis.Map.prototype.zoomCoords = function( x, y, zoom )
{
	this.view.animate( { zoom: zoom, center: [ x, y ], duration: 500 } );
};

/**
 * 
 * @param {type} lon Longitude Float Number
 * @param {type} lat
 * @param {type} zoom
 * @returns {undefined} */
netgis.Map.prototype.zoomLonLat = function( lon, lat, zoom )
{
	this.view.animate( { zoom: zoom, center: ol.proj.fromLonLat( [ lon, lat ], this.view.getProjection() ), duration: 500 } );
};

netgis.Map.prototype.zoomExtentLonLat = function( minlon, minlat, maxlon, maxlat )
{
	var minxy = ol.proj.fromLonLat( [ minlon, minlat ], this.view.getProjection() );
	var maxxy = ol.proj.fromLonLat( [ maxlon, maxlat ], this.view.getProjection() );
	
	this.view.fit( [ minxy[ 0 ], minxy[ 1 ], maxxy[ 0 ], maxxy[ 1 ] ] );
};

netgis.Map.prototype.zoomExtent = function( minx, miny, maxx, maxy )
{
	this.view.fit( [ minx, miny, maxx, maxy ] );
};

netgis.Map.prototype.zoomBBox = function( bbox, anim )
{
	this.view.fit( bbox, { duration: anim } );
};

netgis.Map.prototype.zoomScale = function( scale, anim )
{
	if ( ! anim )
		this.view.setResolution( this.getResolutionFromScale( scale ) );
	else
		this.view.animate( { resolution: this.getResolutionFromScale( scale ), duration: 500 } );
};

netgis.Map.prototype.zoomFeature = function( layerID, featureID )
{
	var layer = this.layers[ layerID ];
	var feature = layer.getSource().getFeatureById( featureID );
	
	this.view.fit( feature.getGeometry().getExtent(), { duration: 500 } );
};

netgis.Map.prototype.zoomFeatures = function( features )
{
	if ( ! features || features.length < 1 ) return;
	
	var extent = features[ 0 ].getGeometry().getExtent();
	
	for ( var i = 1; i < features.length; i++ )
	{
		extent = ol.extent.extend( extent, features[ i ].getGeometry().getExtent() );
	}
	
	this.view.fit( extent, { duration: 0, padding: this.view.padding } );
};

netgis.Map.prototype.addViewHistory = function( center, zoom )
{
	// Check If Last View Is Similar
	if ( this.viewHistory.length > 0 )
	{
		var last = this.viewHistory[ this.viewHistory.length - 1 ];
		var similar = true;
		
		if ( Math.abs( center[ 0 ] - last.center[ 0 ] ) > 10.0 ) similar = false;
		if ( Math.abs( center[ 1 ] - last.center[ 1 ] ) > 10.0 ) similar = false;
		if ( Math.abs( zoom - last.zoom ) > 0.1 ) similar = false;
		
		if ( similar === true ) return;
	}
	
	this.viewHistory.push( { center: center, zoom: zoom } );
	if ( this.viewHistory.length > this.viewHistoryMax ) this.viewHistory.shift();
	
	this.viewIndex = this.viewHistory.length - 1;
};

netgis.Map.prototype.gotoViewHistory = function( i )
{
	if ( this.viewHistory.length < 1 ) return;
	
	var max = this.viewHistory.length - 1;
	if ( i < 0 ) i = max;
	if ( i > max ) i = 0;
	
	if ( i === this.viewIndex ) return;
	
	var view = this.viewHistory[ i ];
	
	this.viewIndex = i;
	this.viewFromHistory = true;
	
	this.view.setCenter( view.center );
	this.view.setZoom( view.zoom );
};

netgis.Map.prototype.setPadding = function( top, right, bottom, left )
{
	var buffer = this.paddingBuffer;
	this.view.padding = [ top + buffer, right + buffer, bottom + buffer, left + buffer ];
};

/**
* 
* @param {format} string Format identifier (jpeg, png, gif)
* @param {resx} integer Map image x resolution (pixels)
* @param {resy} integer Map image y resolution (pixels)
* @param {mode} boolean PDF mode (true = landscape, false = portrait)
* @param {margin} integer PDF page margin (millimeters)
*/
netgis.Map.prototype.exportImage = function( format, resx, resy, mode, margin )
{
	//netgis.util.invoke( this.container, netgis.Events.EXPORT_BEGIN, null );
	
	var self = this;
	var root = this.container;
	var map = this.map;
	
	var config = this.config[ "export" ];
	
	// Request Logo Image
	var logo = new Image();
	
	logo.onload = function()
	{
		//TODO: refactor map render image and image export
		//NOTE: https://github.com/openlayers/openlayers/issues/9100
		//NOTE: scaling / quality bugs when map pixel ratio is not 1.0

		// Render Target
		var renderContainer = document.createElement( "div" );
		renderContainer.style.position = "fixed";
		renderContainer.style.top = "0px";
		renderContainer.style.left = "0px";
		renderContainer.style.width = resx + "px";
		renderContainer.style.height = resy + "px";
		renderContainer.style.background = "white";
		renderContainer.style.zIndex = -1;
		renderContainer.style.opacity = 0.0;
		renderContainer.style.pointerEvents = "none";
		root.appendChild( renderContainer );
		
		map.setTarget( renderContainer );
		
		// Request Render
		map.once
		(
			"rendercomplete",
			function()
			{
				var mapCanvas = document.createElement( "canvas" );
				mapCanvas.width = resx;
				mapCanvas.height = resy;

				var mapContext = mapCanvas.getContext( "2d" );
				mapContext.webkitImageSmoothingEnabled = false;
				mapContext.mozImageSmoothingEnabled = false;
				mapContext.imageSmoothingEnabled = false;

				// Loop Map Layers
				Array.prototype.forEach.call
				(
					document.querySelectorAll( ".ol-layer canvas" ),
					function( canvas )
					{
						if ( canvas.width > 0 )
						{
							var opacity = canvas.parentNode.style.opacity;
							mapContext.globalAlpha = ( opacity === '' ) ? 1.0 : Number( opacity );

							var transform = canvas.style.transform;
							var matrix = transform.match( /^matrix\(([^\(]*)\)$/ )[ 1 ].split( "," ).map( Number );

							CanvasRenderingContext2D.prototype.setTransform.apply( mapContext, matrix );

							mapContext.drawImage( canvas, 0, 0 );
						}
					}
				);

				// Watermark Logo
				mapContext.drawImage( logo, 0, 0 );
				
				// Timestamp
				mapContext.fillStyle = "#fff";
				mapContext.fillRect( 0, mapCanvas.height - 30, 140, 30 );
				mapContext.fillStyle = "#000";
				mapContext.font = "4mm sans-serif";
				mapContext.fillText( netgis.util.getTimeStamp(), 10, mapCanvas.height - 10 );

				// Export Map Image
				var link = document.createElement( "a" );

				switch ( format )
				{
					case "pdf":
					{
						// Dimensions
						var landscape = mode;
						margin = margin ? margin : 0;
						var widthA4 = 297 - margin - margin;
						var heightA4 = 210 - margin - margin;
						var ratio = mapCanvas.width / mapCanvas.height;
						
						if ( ! landscape )
						{
							var w = widthA4;
							widthA4 = heightA4;
							heightA4 = w;
						}

						var width;
						var height;

						if ( mapCanvas.height > mapCanvas.width )
						{
							// Tall Canvas
							height = heightA4;
							width = height * ratio;
							
							if ( width > widthA4 )
							{
								width = widthA4;
								height = width / ratio;
							}
						}
						else
						{
							// Wide Canvas
							width = widthA4;
							height = width / ratio;
							
							if ( height > heightA4 )
							{
								height = heightA4;
								width = height * ratio;
							}
						}

						var pdf = new jsPDF( landscape ? "l" : "p" );

						var x = margin;
						x += ( widthA4 - width ) / 2;

						var y = margin;
						y += ( heightA4 - height ) / 2;

						// Map Image
						pdf.addImage( mapCanvas.toDataURL( "image/png,1.0", 1.0 ), "PNG", x, y, width, height );

						// Text
						pdf.setFillColor( 255, 255, 255 );
						pdf.rect( x, y + height - 11, 80, 11, "F" );
						
						pdf.setFontSize( 8 );
						pdf.text( "Datum: " + netgis.util.getTimeStamp(), x + 2, y + height - 2 - 4 );
						pdf.text( "Quelle: " + window.location.href, x + 2, y + height - 2 );

						// Same Tab
						//pdf.output( "save", { filename: config.export.defaultFilename + ".pdf" } );

						// New Tab (without Name)
						var data = pdf.output( "bloburl", { filename: config[ "default_filename" ] + ".pdf" } );
						window.open( data, "_blank" );

						/*
						// Download (with Name)
						var data = pdf.output( "blob", { filename: config.export.defaultFilename + ".pdf" } );
						var blob = new Blob( [ data ], { type: "octet/stream" } );
						link.setAttribute( "download", "Export.pdf" );
						link.setAttribute( "href", window.URL.createObjectURL( blob ) );
						link.click();
						//window.URL.revokeObjectURL( url );
						*/

						break;
					}

					case "jpeg":
					{					
						if ( window.navigator.msSaveBlob )
						{
							window.navigator.msSaveBlob( mapCanvas.msToBlob(), config[ "default_filename" ] + ".jpg" );
						}
						else
						{
							link.setAttribute( "download", config[ "default_filename" ] + ".jpg" );
							link.setAttribute( "href", mapCanvas.toDataURL( "image/jpeg", 1.0 ) );
							link.click();
						}

						break;
					}

					case "png":
					{					
						if ( window.navigator.msSaveBlob )
						{
							//if ( ! config.export.openNewTab )
								window.navigator.msSaveBlob( mapCanvas.msToBlob(), config[ "default_filename" ] + ".png" );
							/*else
								window.open( mapCanvas.msToBlob(), "_blank" );*/
						}
						else
						{
							/*if ( ! config.export.openNewTab )
							{*/
								link.setAttribute( "download", config[ "default_filename" ] + ".png" );
								link.setAttribute( "href", mapCanvas.toDataURL( "image/png", 1.0 ) );
								link.click();
							/*}
							else
								window.open( mapCanvas.toDataURL( "image/png", 1.0 ), "_blank" );*/
						}

						break;
					}

					case "gif":
					{
						link.setAttribute( "download", config[ "default_filename" ] + ".gif" );
						
						var gif = new GIF( { workerScript: config[ "gif_worker" ], quality: 1 } );
						gif.addFrame( mapCanvas );
						
						gif.on
						(
							"finished",
							function( blob )
							{
								link.setAttribute( "href", window.URL.createObjectURL( blob ) );
								link.click();
							}
						);

						gif.render();

						break;
					}
				}   
				
				// Done
				map.setTarget( root );
				root.removeChild( renderContainer );
				
				netgis.util.invoke( self.container, netgis.Events.EXPORT_END, null );
			}
		);

		// Begin Map Render
		map.renderSync();
	};
	
	// Begin Logo Load & Render
	logo.src = config[ "logo" ];
};

netgis.Map.prototype.exportFeatures = function( nonEdits )
{
	var features = this.editLayer.getSource().getFeatures();
	
	if ( nonEdits === true )
	{
		var nonEditFeatures = this.nonEditLayer.getSource().getFeatures();
		features = features.concat( nonEditFeatures );
	}
	
	var format = new ol.format.GeoJSON();
	var geojson = format.writeFeaturesObject( features, { featureProjection: this.view.getProjection(), dataProjection: "EPSG:4326" } );
	
	var name = this.config[ "export" ][ "default_filename" ] + ".geojson";
	geojson[ "name" ] = name;
	
	netgis.util.downloadJSON( geojson, name );
	
	netgis.util.invoke( this.container, netgis.Events.EXPORT_END, null );
};

netgis.Map.prototype.onClientContextResponse = function( e )
{
	var params = e.detail;
	this.initConfig( params.context.config );
};

netgis.Map.prototype.onEditLayerLoaded = function( e )
{
	var params = e.detail;
	var geojson = params.geojson;
	
	// Parse Features
	var format = new ol.format.GeoJSON();
	var projection = format.readProjection( geojson );
	
	var features = format.readFeatures( geojson, { featureProjection: this.view.getProjection().getCode() } );
	
	// Zoom Features
	var self = this;
	var all = features.slice();
	window.setTimeout( function() { self.zoomFeatures( all ); }, 10 );
	
	// Split Non Editables
	var editables = [];
	
	for ( var i = 0; i < features.length; i++ )
	{
		var feature = features[ i ];
		var props = feature.getProperties();
		
		var editable = props[ "editable" ];
		
		if ( editable === true ) editables.push( feature );
	}
	
	for ( var i = 0; i < editables.length; i++ )
	{
		features.splice( features.indexOf( editables[ i ] ), 1 );
	}
	
	// Add To Layers
	this.editEventsSilent = true;
	
	this.editLayer.getSource().addFeatures( editables );
	this.nonEditLayer.getSource().addFeatures( features );
	
	//this.updateSnapFeatures();
	
	this.editEventsSilent = false;
};

netgis.Map.prototype.onClientSetMode = function( e )
{
	var params = e.detail;
	this.setMode( params.mode );
};

netgis.Map.prototype.onPanelResize = function( e )
{
	var params = e.detail;
	
	this.setPadding( 0, 0, 0, params.width );
	
	this.redrawVectorLayers();
};

netgis.Map.prototype.onPanelToggle = function( e )
{
	var params = e.detail;
	
	/*
	if ( params.visible )
		this.setPadding( 0, 0, 0, params.width );
	else
		this.setPadding( 0, 0, 0, 0 );
	*/
	
	// Check If Any Panel Visible
	var visible = false;
	var width = 0;
	var panels = this.container.parentNode.getElementsByClassName( "netgis-panel" );
	
	// TODO: is this the correct way ?
	
	for ( var i = 0; i < panels.length; i++ )
	{
		if ( panels[ i ].classList.contains( "netgis-show" ) )
		{
			visible = true;
			width = panels[ i ].getBoundingClientRect().width;
			break;
		}
	}
	
	if ( visible )
	{
		this.setPadding( 0, 0, 0, width );
	}
	else
	{
		this.setPadding( 0, 0, 0, 0 );
	}
	
	this.redrawVectorLayers();
};

netgis.Map.prototype.onMapZoom = function( e )
{
	var params = e.detail;
	this.zoom( params.delta );
};

netgis.Map.prototype.onMapZoomHome = function( e )
{
	var config = this.config;
	
	if ( config[ "map" ][ "bbox" ] )
	{
		this.zoomBBox( config[ "map" ][ "bbox" ], 500 );
	}
	else if ( config[ "map" ][ "center" ] )
	{
		var coords = config[ "map" ][ "center" ];
		this.zoomCoords( coords[ 0 ], coords[ 1 ], config[ "map" ][ "zoom" ] );
	}
	else if ( config[ "map" ][ "center_lonlat" ] )
	{
		var coords = config[ "map" ][ "center_lonlat" ];
		this.zoomLonLat( coords[ 0 ], coords[ 1 ], config[ "map" ][ "zoom" ] );
	}
};

netgis.Map.prototype.onMapZoomLonLat = function( e )
{
	var params = e.detail;
	this.zoomLonLat( params.lon, params.lat, params.zoom );
};

netgis.Map.prototype.onMapZoomScale = function( e )
{
	var params = e.detail;
	this.zoomScale( params.scale, params.anim );
};

netgis.Map.prototype.onMapZoomLayer = function( e )
{
	var params = e.detail;
	
	var layer = this.layers[ params.id ];
	
	if ( ! layer ) { console.warning( "trying to zoom non existing layer", params.id ); return; }
	
	this.view.fit( layer.getSource().getExtent(), { duration: 600 } );
};

netgis.Map.prototype.onMapZoomLevel = function( e )
{
	var params = e.detail;
	this.view.setZoom( params.z );
};

netgis.Map.prototype.onMapViewPrev = function( e )
{
	this.gotoViewHistory( this.viewIndex - 1 );
};

netgis.Map.prototype.onMapViewNext = function( e )
{
	this.gotoViewHistory( this.viewIndex + 1 );
};

netgis.Map.prototype.onMapLayerToggle = function( e )
{
	var params = e.detail;
	
	//console.info( "Map Layer Toggle:", params );
	
	switch ( params.id )
	{
		// Internal Layers
		
		case netgis.LayerID.EDITABLE:
		{
			if ( params.on )
				this.map.addLayer( this.editLayer );
			else
				this.map.removeLayer( this.editLayer );
			
			break;
		}
		
		case netgis.LayerID.NON_EDITABLE:
		{
			if ( params.on )
				this.map.addLayer( this.nonEditLayer );
			else
				this.map.removeLayer( this.nonEditLayer );
			
			break;
		}
		
		// Config Layers
		
		default:
		{
			if ( params.on )
			{
				var layers = this.config[ "layers" ];

				for ( var i = 0; i < layers.length; i++ )
				{
					var layer = layers[ i ];

					if ( layer[ "id" ] !== params.id ) continue;

					this.addLayer( params.id, layer );
				}
			}
			else
			{
				this.removeLayer( params.id );
			}
			
			break;
		}
	}
};

netgis.Map.prototype.onMapLayerTransparency = function( e )
{
	var params = e.detail;
	var layer = this.layers[ params.id ];
	
	if ( ! layer ) //return; // TODO: toggle layer on ?
	{
		netgis.util.invoke( this.container, netgis.Events.MAP_LAYER_TOGGLE, { id: params.id, on: true } );
		layer = this.layers[ params.id ];
	}
	
	layer.setOpacity( 1.0 - params.transparency );
};

netgis.Map.prototype.onMapSnapToggle = function( e )
{
	var params = e.detail;
	this.setSnapping( params.on );
};

netgis.Map.prototype.onMapMoveEnd = function( e )
{
	var center = this.view.getCenter();
	var zoom = this.view.getZoom();
	var scale = this.getScaleFromResolution( this.view.getResolution() );
	
	if ( this.viewFromHistory === false )
	{
		this.addViewHistory( center, zoom );
	}
	
	netgis.util.invoke( this.container, netgis.Events.MAP_VIEW_CHANGE, { center: center, zoom: zoom, scale: scale } );
	
	this.viewFromHistory = false;
};

netgis.Map.prototype.onPointerMove = function( e )
{
	var pixel = e.pixel;
	var coords = e.coordinate;
	
	var hoverFeature = null;
	var hoverLayer = null;
	var hoverBounds = undefined;
	
	var self = this;
	
	this.map.forEachFeatureAtPixel
	(
		pixel,
		function( feature, layer )
		{
			if ( ! layer ) return;
			if ( layer === self.measureLayer ) return;
			if ( layer === self.nonEditLayer ) return;
			if ( layer === self.boundsLayer ) { hoverBounds = feature; return; }
			if ( layer === self.previewLayer ) return;
			
			// TODO: no hover/interaction on imported layers for now
			////if ( layer.get( "id" ) && netgis.util.isString( layer.get( "id" ) ) && layer.get( "id" ).search( "import-" ) !== -1 ) return false;
			
			hoverFeature = feature;
			hoverLayer = layer;

			return true;
		}
	);
	
	// Handle Interactions
	switch ( this.mode )
	{
		case netgis.Modes.VIEW:
		{
			// Clickable Query Cursor
			var queryables = this.getQueryableLayers( true );
			
			if ( queryables.length === 0 )
			{
				// No Queryable Layers But Hover Feature
				if ( hoverFeature )
					this.container.classList.add( "netgis-clickable" );
				else
					this.container.classList.remove( "netgis-clickable" );
			}
			else
			{
				// Has Queryable Layers
				this.container.classList.add( "netgis-clickable" );
			}
			
			break;
		}
		
		case netgis.Modes.DRAW_POINTS:
		case netgis.Modes.DRAW_LINES:
		{
			this.updateDrawBufferPreview();
			break;
		}
	}
	
	// Inside Allowed Bounds
	if ( this.boundsLayer && ( this.mode === netgis.Modes.DRAW_POINTS || this.mode === netgis.Modes.DRAW_LINES || this.mode === netgis.Modes.DRAW_POLYGONS ) )
	{
		//if ( hoverBounds !== this.hoverBounds )
		{
			if ( hoverBounds )
			{
				this.container.classList.remove( "netgis-not-allowed" );
				this.container.removeAttribute( "title" );
			}
			else
			{
				this.container.classList.add( "netgis-not-allowed" );
				
				var message = this.config[ "tools" ][ "bounds_message" ];
				if ( message && message.length > 0 ) this.container.setAttribute( "title", message );
			}

			this.hoverBounds = hoverBounds;
		}
	}
	
	// Update Feature States
	var hoverable = true;
	
	////if ( this.mode === netgis.Modes.VIEW ) hoverable = false;
	if ( this.mode === netgis.Modes.MEASURE_LINE ) hoverable = false;
	if ( this.mode === netgis.Modes.MEASURE_AREA ) hoverable = false;
	if ( this.mode === netgis.Modes.DRAW_POINTS ) hoverable = false;
	if ( this.mode === netgis.Modes.DRAW_LINES ) hoverable = false;
	if ( this.mode === netgis.Modes.DRAW_POLYGONS ) hoverable = false;
	if ( this.mode === netgis.Modes.CUT_FEATURES_DRAW ) hoverable = false;
	
	if ( hoverFeature !== this.hoverFeature && hoverable )
	{
		if ( this.hoverFeature )
		{
			// Leave
			this.onFeatureLeave( this.hoverFeature, this.hoverLayer, pixel, coords );
		}
		
		if ( hoverFeature )
		{
			// Enter
			this.onFeatureEnter( hoverFeature, hoverLayer, pixel, coords );
		}
		
		this.redrawVectorLayers();
		
		this.hoverFeature = hoverFeature;
		this.hoverLayer = hoverLayer;
	}
	
	if ( hoverFeature )
	{
		// Hover
		this.onFeatureHover( hoverFeature, hoverLayer, pixel, coords );
	}
};

netgis.Map.prototype.onPointerLeave = function( e )
{
	if ( ! this.hoverFeature ) return;
	
	var pixel = [ e.offsetX, e.offsetY ];
	this.onFeatureLeave( this.hoverFeature, this.hoverLayer, pixel, null );
	
	this.hoverFeature = null;
	this.hoverLayer = null;
};

netgis.Map.prototype.onPointerClick = function( e )
{
	var pixel = e.pixel;
	var coords = e.coordinate;
	
	////this.selectedFeatures = [];
	
	this.popupOverlay.setPosition( coords );
	
	// Map Click Event
	var view =
	{
		resolution: this.view.getResolution(),
		projection: this.view.getProjection().getCode(),
		bbox: this.view.calculateExtent( this.map.getSize() ),
		width: this.map.getSize()[ 0 ],
		height: this.map.getSize()[ 1 ]
	};
	
	var lonlat = ol.proj.toLonLat( coords, this.view.getProjection() );
	
	var params =
	{
		mode: this.mode,
		pixel: pixel,
		coords: coords,
		lon: lonlat[ 0 ],
		lat: lonlat[ 1 ],
		overlay: this.popupOverlay.getElement(),
		view: view
	};
	
	if ( this.mode === netgis.Modes.VIEW ) netgis.util.invoke( this.container, netgis.Events.MAP_CLICK, params );
	
	// Check Clicked Features
	var features = [];
	
	var self = this;
	this.map.forEachFeatureAtPixel
	(
		pixel,
		function( feature, layer )
		{
			if ( ! layer ) return;
			if ( layer === self.nonEditLayer ) return;
			if ( layer === self.boundsLayer ) return;
			if ( layer === self.measureLayer ) return;
			if ( layer === self.previewLayer ) return;
			
			// TODO: init seperate sketch layer ?
			//if ( layer === self.sketchLayer ) return;
			
			if ( self.sketchFeatures.indexOf( feature ) > -1 ) return;
			
			features.push( { feature: feature, layer: layer } );
		}
	);
	
	// Selectable Mode
	var selectable = true;
	
	if ( this.mode === netgis.Modes.VIEW ) selectable = false;
	if ( this.mode === netgis.Modes.MEASURE_LINE ) selectable = false;
	if ( this.mode === netgis.Modes.MEASURE_AREA ) selectable = false;
	if ( this.mode === netgis.Modes.DRAW_POINTS ) selectable = false;
	if ( this.mode === netgis.Modes.DRAW_LINES ) selectable = false;
	if ( this.mode === netgis.Modes.DRAW_POLYGONS ) selectable = false;
	if ( this.mode === netgis.Modes.CUT_FEATURES_DRAW ) selectable = false;
	
	if ( selectable )
	{
		// Clear Previous Selection If Not Multiple
		if ( features.length > 0 && this.selectMultiple === false )
			this.selectedFeatures = [];

		// Deselect If Nothing Clicked
		if ( features.length === 0 && this.selectMultiple === false )
			this.selectedFeatures = [];
		
		// Deselect If Multi Reset Requested
		if ( this.selectReset === true )
		{
			this.selectedFeatures = [];
			this.selectReset = false;
		}
		
		if ( this.mode === netgis.Modes.BUFFER_FEATURES_DYNAMIC )
		{
			this.updateBufferFeaturesSketch( this.bufferFeaturesRadius, this.bufferFeaturesSegments );
		}
	}
	
	// Feature Clicked
	for ( var i = 0; i < features.length; i++ )
	{
		var feature = features[ i ];
		
		if ( selectable )
		{
			// Check Already Selected
			var found = this.selectedFeatures.indexOf( feature.feature );
			
			if ( found > -1 )
			{
				// Remove From Selection
				this.selectedFeatures.splice( found, 1 );
			}
			else
			{
				// Add To Selection
				this.selectedFeatures.push( feature.feature );
			}
		}
		
		this.onFeatureClick( feature.feature, feature.layer, pixel, coords );
		//if ( this.mode === netgis.Modes.VIEW ) this.onFeatureClick( feature.feature, feature.layer, pixel, coords );
	}
	
	// Render
	this.redrawVectorLayers();
};

netgis.Map.prototype.onContainerClick = function( e )
{
	var clicks = e.detail;
	
	if ( clicks === 2 )
	{
		this.onDoubleClick( e );
	}
};

netgis.Map.prototype.onDoubleClick = function( e )
{
	switch ( this.mode )
	{
		case netgis.Modes.MEASURE_LINE:
		{
			this.interactions[ netgis.Modes.MEASURE_LINE ][ 2 ].finishDrawing();
			break;
		}
		
		case netgis.Modes.MEASURE_AREA:
		{
			this.interactions[ netgis.Modes.MEASURE_AREA ][ 2 ].finishDrawing();
			break;
		}
		
		case netgis.Modes.DRAW_LINES:
		{
			this.interactions[ netgis.Modes.DRAW_LINES ][ 0 ].finishDrawing();
			break;
		}
		
		case netgis.Modes.DRAW_POLYGONS:
		{
			this.interactions[ netgis.Modes.DRAW_POLYGONS ][ 0 ].finishDrawing();
			break;
		}
		
		case netgis.Modes.CUT_FEATURES_DRAW:
		{
			this.interactions[ netgis.Modes.CUT_FEATURES_DRAW ][ 0 ].finishDrawing();
			break;
		}
	}
};

netgis.Map.prototype.onRightClick = function( e )
{
	switch ( this.mode )
	{
		case netgis.Modes.MEASURE_LINE:
		{
			this.interactions[ netgis.Modes.MEASURE_LINE ][ 2 ].finishDrawing();
			break;
		}
		
		case netgis.Modes.MEASURE_AREA:
		{
			this.interactions[ netgis.Modes.MEASURE_AREA ][ 2 ].finishDrawing();
			break;
		}
		
		case netgis.Modes.DRAW_LINES:
		{
			this.interactions[ netgis.Modes.DRAW_LINES ][ 0 ].finishDrawing();
			break;
		}
		
		case netgis.Modes.DRAW_POLYGONS:
		{
			this.interactions[ netgis.Modes.DRAW_POLYGONS ][ 0 ].finishDrawing();
			break;
		}
		
		case netgis.Modes.CUT_FEATURES_DRAW:
		{
			this.interactions[ netgis.Modes.CUT_FEATURES_DRAW ][ 0 ].finishDrawing();
			break;
		}
	}
	
	e.preventDefault();
	return false;
};

netgis.Map.prototype.onKeyDown = function( e )
{	
	var keycode = e.keyCode || e.which;
	
	var KEY_ENTER = 13;
	var KEY_ESCAPE = 27;
	var KEY_BACK = 8;
	var KEY_DEL = 46;
	var KEY_SHIFT = 16;
	
	//console.info( "Key Press:", keycode );
	
	switch ( this.mode )
	{
		case netgis.Modes.MEASURE_LINE:
		{
			if ( keycode === KEY_ENTER ) this.interactions[ netgis.Modes.MEASURE_LINE ][ 2 ].finishDrawing();
			if ( keycode === KEY_ESCAPE ) this.interactions[ netgis.Modes.MEASURE_LINE ][ 2 ].abortDrawing();
			break;
		}
		
		case netgis.Modes.MEASURE_AREA:
		{
			if ( keycode === KEY_ENTER ) this.interactions[ netgis.Modes.MEASURE_AREA ][ 2 ].finishDrawing();
			if ( keycode === KEY_ESCAPE ) this.interactions[ netgis.Modes.MEASURE_AREA ][ 2 ].abortDrawing();
			break;
		}
		
		case netgis.Modes.DRAW_LINES:
		{
			var draw = this.interactions[ netgis.Modes.DRAW_LINES ][ 0 ];
			if ( keycode === KEY_ENTER ) draw.finishDrawing();
			if ( keycode === KEY_ESCAPE ) draw.abortDrawing();
			if ( keycode === KEY_BACK ) draw.removeLastPoint();
			if ( keycode === KEY_DEL ) draw.abortDrawing();
			break;
		}
		
		case netgis.Modes.DRAW_POLYGONS:
		{
			var draw = this.interactions[ netgis.Modes.DRAW_POLYGONS ][ 0 ];
			if ( keycode === KEY_ENTER ) draw.finishDrawing();
			if ( keycode === KEY_ESCAPE ) draw.abortDrawing();
			if ( keycode === KEY_BACK ) draw.removeLastPoint();
			if ( keycode === KEY_DEL ) draw.abortDrawing();
			break;
		}
		
		case netgis.Modes.CUT_FEATURES_DRAW:
		{
			var draw = this.interactions[ netgis.Modes.CUT_FEATURES_DRAW ][ 0 ];
			if ( keycode === KEY_ENTER ) draw.finishDrawing();
			if ( keycode === KEY_ESCAPE ) draw.abortDrawing();
			if ( keycode === KEY_BACK ) draw.removeLastPoint();
			if ( keycode === KEY_DEL ) draw.abortDrawing();
			
			// Back To Select Mode While Shift Down
			if ( keycode === KEY_SHIFT ) netgis.util.invoke( this.container, netgis.Events.CLIENT_SET_MODE, { mode: netgis.Modes.CUT_FEATURES } );
			
			break;
		}
	}
	
	if ( keycode === KEY_SHIFT )
	{
		////this.selectMultiple = true;
		
		if ( this.selectMultiple === false )
			netgis.util.invoke( this.container, netgis.Events.SELECT_MULTI_TOGGLE, { on: true } );
	}
	
	//this.redrawVectorLayers();
};

netgis.Map.prototype.onKeyUp = function( e )
{
	var keycode = e.keyCode || e.which;
	
	var KEY_SHIFT = 16;
	
	switch ( this.mode )
	{
		case netgis.Modes.BUFFER_FEATURES:
		{
			if ( this.selectMultiple )
			{
				netgis.util.invoke( this.container, netgis.Events.CLIENT_SET_MODE, { mode: netgis.Modes.BUFFER_FEATURES_EDIT } );
			}
			
			break;
		}
		
		case netgis.Modes.CUT_FEATURES:
		{
			if ( this.selectMultiple )
			{
				netgis.util.invoke( this.container, netgis.Events.CLIENT_SET_MODE, { mode: netgis.Modes.CUT_FEATURES_DRAW } );
			}
			
			break;
		}
	}
	
	if ( keycode === KEY_SHIFT )
	{
		////this.selectMultiple = false;
		netgis.util.invoke( this.container, netgis.Events.SELECT_MULTI_TOGGLE, { on: false } );
		
		// TODO: deprecated ?
		this.selectReset = false;
		
		if ( this.config[ "tools" ][ "select_multi_reset" ] === true )
		{
			this.selectReset = true;
		}
	}
};

netgis.Map.prototype.onFeatureEnter = function( feature, layer, pixel, coords )
{
	//console.info( "FEATURE ENTER:", arguments );
	
	if ( ! layer ) return;
	
	////this.container.classList.add( "netgis-clickable" );
	
	//console.info( "LAYER:", layer.get( "id" ) );
	
	var hoverable = false;
	/*if ( layer.get( "id" ) === netgis.Client.Layers.EDIT_LAYER ) hoverable = true;
	if ( layer.get( "id" ) === netgis.Client.Layers.PARCEL_DISTRICTS ) hoverable = true;
	if ( layer.get( "id" ) === netgis.Client.Layers.PARCEL_FIELDS ) hoverable = true;
	if ( layer.get( "id" ) === netgis.Client.Layers.PARCEL_FEATURES ) hoverable = true;*/
	
	if ( layer === this.editLayer ) hoverable = true;
	
	//console.info( "Feature Enter:", feature, layer, hoverable );
	
	//if ( netgis.util.isString( layer.get( "id" ) ) && layer.get( "id" ).search( "import-" ) > -1 ) hoverable = false;
	
	//if ( layer.get( "id" ) !== netgis.Client.Layers.EDIT_LAYER && ( netgis.util.isString( layer.get( "id" ) ) && layer.get( "id" ).search( "import-" ) === -1 ) )
	if ( hoverable )
	{
		////feature.setStyle( this.styleHover.bind( this ) );
		
		
	}

	//netgis.util.invoke( this.container, netgis.Events.MAP_FEATURE_ENTER, { pixel: pixel, coords: coords, layer: layer.get( "id" ), properties: feature.getProperties() } );
	
	switch ( this.mode )
	{
		case netgis.Modes.VIEW:
		{
			this.container.classList.add( "netgis-clickable" );
			
			break;
		}
		
		case netgis.Modes.DELETE_FEATURES:
		case netgis.Modes.BUFFER_FEATURES:
		case netgis.Modes.BUFFER_FEATURES_DYNAMIC:
		case netgis.Modes.CUT_FEATURES:
		{
			this.container.classList.add( "netgis-clickable" );
			feature.setStyle( this.styleHover.bind( this ) );
			break;
		}
		
		case netgis.Modes.SEARCH_PARCEL:
		{
			this.container.classList.add( "netgis-clickable" );
			feature.setStyle( this.styleHover.bind( this ) );
			break;
		}
	}
	
	netgis.util.invoke( this.container, netgis.Events.MAP_FEATURE_ENTER, { pixel: pixel, coords: coords, layer: layer.get( "id" ), properties: feature.getProperties() } );
};

netgis.Map.prototype.onFeatureHover = function( feature, layer, pixel, coords )
{
	
};

netgis.Map.prototype.onFeatureClick = function( feature, layer, pixel, coords )
{
	//console.info( "FEATURE CLICK:", this.selectedFeatures );
	
	var lonlat = ol.proj.toLonLat( coords, this.view.getProjection() );
	
	var params =
	{
		pixel: pixel,
		coords: coords,
		lon: lonlat[ 0 ],
		lat: lonlat[ 1 ],
		/*
		layer: this.hoverLayer ? this.hoverLayer.get( "id" ) : null,
		id: this.hoverFeature ? this.hoverFeature.getId() : null,
		properties: this.hoverFeature ? this.hoverFeature.getProperties() : null
		*/
		layer: layer.get( "id" ),
		id: feature.getId(),
		properties: feature.getProperties()
	};
	
	////netgis.util.invoke( this.container, netgis.Events.MAP_FEATURE_CLICK, params );
	
	// Handle Interactions
	switch ( this.mode )
	{
		case netgis.Modes.VIEW:
		{
			netgis.util.invoke( this.container, netgis.Events.MAP_FEATURE_CLICK, params );
			
			break;
		}
		
		case netgis.Modes.DELETE_FEATURES:
		{
			layer.getSource().removeFeature( feature );
			this.onFeatureLeave( feature, layer );
			netgis.util.invoke( this.container, netgis.Events.CLIENT_SET_MODE, { mode: netgis.Modes.VIEW } );
			
			break;
		}
		
		case netgis.Modes.BUFFER_FEATURES:
		case netgis.Modes.BUFFER_FEATURES_EDIT:
		{
			this.onFeatureLeave( feature, layer );
			
			if ( ! this.selectMultiple )
				netgis.util.invoke( this.container, netgis.Events.CLIENT_SET_MODE, { mode: netgis.Modes.BUFFER_FEATURES_EDIT } );
			else
				netgis.util.invoke( this.container, netgis.Events.CLIENT_SET_MODE, { mode: netgis.Modes.BUFFER_FEATURES } );
			
			break;
		}
		
		case netgis.Modes.BUFFER_FEATURES_DYNAMIC:
		{
			this.updateBufferFeaturesSketch( this.bufferFeaturesRadius, this.bufferFeaturesSegments );
			break;
		}
		
		case netgis.Modes.CUT_FEATURES:
		{
			if ( feature.getGeometry() instanceof ol.geom.Point )
			{
				//window.alert( "Trying to cut a point feature!" );
				this.onFeatureLeave( feature, layer );
				break;
			}
			
			////this.onFeatureLeave( feature, layer );
			
			if ( ! this.selectMultiple )
				netgis.util.invoke( this.container, netgis.Events.CLIENT_SET_MODE, { mode: netgis.Modes.CUT_FEATURES_DRAW } );
			
			break;
		}
	}
};

netgis.Map.prototype.onFeatureLeave = function( feature, layer, pixel, coords )
{
	////if ( ! layer ) return;
	
	//this.container.classList.remove( "netgis-clickable" );
	
	//console.info( "Feature Leave:", feature );
	
	/*if ( layer.get( "id" ) !== netgis.Client.Layers.EDIT_LAYER )
	{
		feature.setStyle( null );
	}*/
	
	var hoverable = false;
	if ( layer === this.editLayer ) hoverable = true;
	
	if ( hoverable )
	{
		////feature.setStyle( null );
	}
			
	netgis.util.invoke( this.container, netgis.Events.MAP_FEATURE_LEAVE, { pixel: pixel, coords: coords, layer: layer ? layer.get( "id" ) : null, properties: feature.getProperties() } );
	
	switch ( this.mode )
	{
		case netgis.Modes.VIEW:
		{
			break;
		}
		
		case netgis.Modes.DELETE_FEATURES:
		case netgis.Modes.BUFFER_FEATURES:
		case netgis.Modes.BUFFER_FEATURES_DYNAMIC:
		case netgis.Modes.CUT_FEATURES:
		case netgis.Modes.CUT_FEATURES_DRAW:
		{
			this.container.classList.remove( "netgis-clickable" );
			feature.setStyle( null );
			break;
		}
		
		case netgis.Modes.SEARCH_PARCEL:
		{
			this.container.classList.remove( "netgis-clickable" );
			feature.setStyle( null );
			break;
		}
	}
};

netgis.Map.prototype.onEditLayerAdd = function( e )
{	
	////this.updateEditOutput();
	//this.updateEditLayerItem();
	
	
	if ( ! this.editEventsSilent ) this.updateEditOutput();
	
	this.snapFeatures.push( e.feature );
};

netgis.Map.prototype.onEditLayerRemove = function( e )
{
	if ( ! this.editEventsSilent ) this.updateEditOutput();
	
	this.snapFeatures.remove( e.feature );
};

netgis.Map.prototype.onEditLayerChange = function( e )
{
	if ( ! this.editEventsSilent ) this.updateEditOutput();
};

netgis.Map.prototype.onCopyFeatureToEdit = function( e )
{
	var params = e.detail;
	
	var layer = this.layers[ params.source ];
	var feature = layer.getSource().getFeatureById( params.id );
	
	if ( ! feature ) { console.error( "feature to copy not found", params ); return; }
	
	if ( ! this.editLayer.getSource().getFeatureById( params.id ) )
	{
		feature.setStyle( undefined );
		this.selectedFeatures = [];
		
		this.editLayer.getSource().addFeature( feature );
	}
};

netgis.Map.prototype.onGeolocToggleActive = function( e )
{
	var params = e.detail;
	
	if ( params.on )
		this.geolocLayer.setVisible( true );
	else
		this.geolocLayer.setVisible( false );
};

netgis.Map.prototype.onGeolocChange = function( e )
{
	var params = e.detail;
	
	var marker = this.geolocLayer.getSource().getFeatures()[ 0 ];
	marker.getGeometry().setCoordinates( ol.proj.fromLonLat( [ params.lon, params.lat ], this.view.getProjection() ) );
	
	if ( params.center === true )
	{
		this.zoomLonLat( params.lon, params.lat, this.view.getZoom() );
	}
};

netgis.Map.prototype.onMeasureLineBegin = function( e )
{
	this.measureLayer.getSource().clear();
};

netgis.Map.prototype.onMeasureAreaBegin = function( e )
{
	this.measureLayer.getSource().clear();
};
/*
netgis.Map.prototype.onMeasureEnd = function( e )
{
	var geom = e.feature.getGeometry();
	var l = geom.getLength();
	
	console.info( "Measure End:", l );
};
*/

netgis.Map.prototype.onMeasureClear = function( e )
{
	this.measureLayer.getSource().clear();
};

netgis.Map.prototype.onDrawBufferEnd = function( e )
{
	var feature = e.feature;
	var previews = this.previewLayer.getSource().getFeatures();
	
	if ( previews.length === 0 ) return;
	
	var preview = previews[ 0 ];
	
	// Check Point Inside Bounds Layer
	if ( this.boundsLayer )
	{
		var inside = true;
		
		if ( feature.getGeometry() instanceof ol.geom.Point )
			inside = this.isPointInsideLayer( this.boundsLayer, feature.getGeometry().getCoordinates() );
		else
			inside = this.isGeomInsideLayer( this.boundsLayer, feature.getGeometry() );

		if ( ! inside ) return;
		
		// Clip Buffer Preview Against Bounds
		var parser = new jsts.io.OL3Parser();

		var a = parser.read( preview.getGeometry() );
		var bounds = this.boundsLayer.getSource().getFeatures();
		
		for ( var i = 0; i < bounds.length; i++ )
		{
			var b = parser.read( bounds[ i ].getGeometry() );
			
			if ( ! a.intersects( b ) ) continue;
			
			a = a.intersection( b );
		}
		
		// TODO: handle preview intersection with multiple bounds features, create multi polygon ?

		// Output
		var geom = parser.write( a );
		preview.setGeometry( geom );
	}
		
	// Add Buffer Feature
	var src = this.editLayer.getSource();
	src.addFeature( preview.clone() );

	// Remove Sketch Feature
	window.setTimeout
	(
		function()
		{
			src.removeFeature( feature );
		},
		10
	);
};

/*
netgis.Map.prototype.onDrawLinesEnd = function( e )
{
	var preview = this.previewLayer.getSource().getFeatures()[ 0 ];
	
	if ( preview )
	{
		var src = this.editLayer.getSource();
		
		// Add Buffer Feature
		src.addFeature( preview.clone() );
	
		// Remove Sketch Feature
		window.setTimeout
		(
			function()
			{
				src.removeFeature( e.feature );
			},
			10
		);
	}
};
*/

netgis.Map.prototype.onSelectMultiToggle = function( e )
{
	var params = e.detail;
	this.selectMultiple = params.on;
};

netgis.Map.prototype.onDrawBufferToggle = function( e )
{
	var params = e.detail;
	
	this.setDrawBuffer( params.on, params.radius, params.segments );
	
	/*
	console.info( "DRAW BUFFER:", params );
	
	this.drawBufferOn = params.on;
	
	if ( params.on )
	{
		var feature = this.createBufferFeature( new ol.geom.Point( this.view.getCenter() ), params.radius, params.segments );
		this.previewLayer.getSource().addFeature( feature );
		
		this.drawBufferRadius = params.radius;
		this.drawBufferSegments = params.segments;
	}
	else
	{
		this.previewLayer.getSource().clear();
	}
	*/
};

netgis.Map.prototype.onDrawBufferChange = function( e )
{
	var params = e.detail;
	
	this.drawBufferRadius = params.radius;
	this.drawBufferSegments = params.segments;
	
	this.updateDrawBufferPreview();
};

netgis.Map.prototype.onBufferChange = function( e )
{
	var params = e.detail;
   
	this.updateBufferFeaturesSketch( params.radius, params.segments );
	
	this.bufferFeaturesRadius = params.radius;
	this.bufferFeaturesSegments = params.segments;
};

netgis.Map.prototype.updateBufferFeaturesSketch = function( radius, segments )
{
	var features = this.selectedFeatures;
	var source = this.editLayer.getSource();
	
	this.clearSketchFeatures();
	
	for ( var f = 0; f < features.length; f++ )
	{
		var target = this.selectedFeatures[ f ];
		var feature = this.createBufferFeature( target.getGeometry(), radius, segments );
		
		source.addFeature( feature );
		
		this.sketchFeatures.push( feature );
	}
};

netgis.Map.prototype.onBufferAccept = function( e )
{
	// Remove Selected Source Features
	var features = this.selectedFeatures;
	var source = this.editLayer.getSource();
	
	for ( var f = 0; f < features.length; f++ )
	{
		var feature = features[ f ];
		source.removeFeature( feature );
	}
	
	// Keep Sketch Features in Edit Layer
	this.sketchFeatures = [];
	this.selectedFeatures = [];
};

netgis.Map.prototype.onCutFeaturesDrawEnd = function( e )
{
	var cutter = e.feature;
	//var target = this.selectedFeatures[ 0 ];
	
	//console.info( "CUT END:", this.selectedFeatures );
	
	for ( var i = 0; i < this.selectedFeatures.length; i++ )
	{
		var target = this.selectedFeatures[ i ];
	
		if ( target )
		{
			this.onFeatureLeave( target, null );

			// Check Geom Type
			if ( target.getGeometry() instanceof ol.geom.Point )
			{
				console.error( "trying to cut a point feature", target );
			}
			else
			{
				// Cut Process
				var parser = new jsts.io.OL3Parser();

				var a = parser.read( target.getGeometry() );
				var b = parser.read( cutter.getGeometry() );

				var c = a.difference( b );

				// Output
				var geom = parser.write( c );
				var feature = new ol.Feature( { geometry: geom } );

				var source = this.editLayer.getSource();
				source.removeFeature( target );
				source.addFeature( feature );

				//this.selectedFeatures[ 0 ] = feature;
			}
		}
	
	}
	
	this.selectedFeatures = [];
	
	this.editEventsSilent = true;
	this.splitMultiPolygons( this.editLayer );
	this.editEventsSilent = false;
	this.updateEditOutput();
	
	netgis.util.invoke( this.container, netgis.Events.CLIENT_SET_MODE, { mode: netgis.Modes.VIEW } );
};

netgis.Map.prototype.onImportLayerAccept = function( e )
{
	var params = e.detail;
	
	var layer = this.addLayer( params[ "id" ], params );
	var source = layer.getSource();
	
	if ( source instanceof ol.source.Vector && source.getFeatures().length > 0 )
	{
		//if ( source.getFeatures().length === 0 ) return;
		this.view.fit( layer.getSource().getExtent(), { duration: 600 } );
	}
};

netgis.Map.prototype.onImportGeoportalSubmit = function( e )
{
	var params = e.detail;
	
	//console.info( "Map Geoportal Submit:", params );
	
	/*var id = params.layer.id;
	
	var config =
	{
		type: netgis.LayerTypes.WMS,
		url: params.layer.url,
		name: params.layer.name,
		order: 10000
	};
	
	this.addLayer( id, config );*/
};

netgis.Map.prototype.onImportLayerPreview = function( e )
{
	var params = e.detail;
	
	var layer = this.createLayer( params );
	var proj = this.view.getProjection().getCode(); // TODO: layer.getSource().getProjection().getCode() ?
	
	netgis.util.invoke( this.container, netgis.Events.IMPORT_LAYER_PREVIEW_FEATURES, { id: params.id, title: params.title, layer: layer, proj: proj } );
};

netgis.Map.prototype.onSearchParcelReset = function( e )
{
	// Zoom To Parcel Districts
	var zoom = this.config[ "searchparcel" ][ "districts_service" ][ "min_zoom" ];
	if ( zoom ) this.view.setZoom( zoom );
};

/*
netgis.Map.prototype.onSearchParcelFieldsResponse = function( e )
{
	var params = e.detail;
	
	console.info( "Fields:", params );
	
	
};
*/

netgis.Map.prototype.onSearchParcelItemEnter = function( e )
{
	var params = e.detail;
	var id = params.id;
	
	//console.info( "PARCEL ENTER:", id );
	
	var feature = this.layers[ "searchparcel_parcels" ].getSource().getFeatureById( id );
	
	feature.setStyle( this.styleHover.bind( this ) );
};

netgis.Map.prototype.onSearchParcelItemLeave = function( e )
{
	var params = e.detail;
	var id = params.id;
	
	//console.info( "PARCEL LEAVE:", id );
	
	var feature = this.layers[ "searchparcel_parcels" ].getSource().getFeatureById( id );
	
	feature.setStyle( null );
};

netgis.Map.prototype.onSearchParcelItemClick = function( e )
{
	var params = e.detail;
	
	var id = params.id;
	
	console.info( "PARCEL CLICK:", id );
	
	this.zoomFeature( "searchparcel_parcels", id );
};

netgis.Map.prototype.onSearchParcelItemImport = function( e )
{
	var params = e.detail;
	
	
};

netgis.Map.prototype.onDrawPointsUpdateGeom = function( coords, geom, proj )
{
	if ( ! geom )
		geom = new ol.geom.Point( coords );
	else
		geom.setCoordinates( coords );
	
	// TODO: can not use this for interactive bounds check because not fired on move ?
	
	return geom;
};

netgis.Map.prototype.onDrawLinesUpdateGeom = function( coords, geom, proj )
{	
	// NOTE: https://openlayers.org/en/latest/apidoc/module-ol_geom_LineString-LineString.html
	// NOTE: https://openlayers.org/en/latest/apidoc/module-ol_interaction_Draw-Draw.html
	// NOTE: https://openlayers.org/en/latest/apidoc/module-ol_interaction_Draw.html#~GeometryFunction
	// NOTE: https://gis.stackexchange.com/questions/165971/openlayers-drawing-interaction-geometryfunction
	
	if ( ! geom )
		geom = new ol.geom.LineString( coords );
	else
		geom.setCoordinates( coords );
	
	// Check Layer Contains Geom
	var inside = this.isGeomInsideLayer( this.boundsLayer, geom );
	
	this.drawError = ! inside;
	
	// TODO: remove last coord if not inside ?
	
	return geom;
};

netgis.Map.prototype.onDrawPolygonsUpdateGeom = function( coords, geom, proj )
{
	if ( ! geom )
	{
		geom = new ol.geom.Polygon( coords );
	}
	else
	{
		coords = [ coords[ 0 ].concat( [ coords[ 0 ][ 0 ] ] ) ];
		geom.setCoordinates( coords );
	}
	
	// Check Layer Contains Geom
	var inside = true;
	
	if ( coords[ 0 ].length < 4 )
	{
		// Not Yet A Polygon
		for ( var i = 0; i < coords[ 0 ].length; i++ )
		{
			var c = coords[ 0 ][ i ];
			
			// TODO: check line between first two points instead of single points ?
			
			if ( ! this.isPointInsideLayer( this.boundsLayer, c ) )
			{
				inside = false;
				break;
			}
		}
	}
	else
	{
		// Complete Polygon Check
		inside = this.isGeomInsideLayer( this.boundsLayer, geom );
	}
	
	this.drawError = ! inside;
	
	return geom;
};

netgis.Map.prototype.onDrawPointsEnd = function( e )
{
	// NOTE: https://gis.stackexchange.com/questions/252045/openlayers-map-draw-polygon-feature-inside-boundaries
	
	// Check Point Inside Bounds Layer
	if ( ! this.boundsLayer ) return;
	
	//var action = e.target;
	var feature = e.feature;
	var layer = this.editLayer;
	
	var inside = this.isPointInsideLayer( this.boundsLayer, feature.getGeometry().getCoordinates() );
	
	// TODO: refactor with line / polygon draw end functions ?
	
	if ( ! inside )
	{
		window.setTimeout( function() { layer.getSource().removeFeature( feature ); }, 10 );
	}
};

netgis.Map.prototype.onDrawLinesEnd = function( e )
{
	// Check Line Inside Bounds Layer
	if ( ! this.boundsLayer ) return;
	
	var feature = e.feature;
	var layer = this.editLayer;
	
	var inside = this.isGeomInsideLayer( this.boundsLayer, feature.getGeometry() );
	
	if ( ! inside )
	{
		window.setTimeout( function() { layer.getSource().removeFeature( feature ); }, 10 );
	}
};

netgis.Map.prototype.onDrawPolygonsEnd = function( e )
{
	// Check Polygon Inside Bounds Layer
	if ( ! this.boundsLayer ) return;
	
	var feature = e.feature;
	var layer = this.editLayer;
	
	var inside = this.isGeomInsideLayer( this.boundsLayer, feature.getGeometry() );
	
	if ( ! inside )
	{
		window.setTimeout( function() { layer.getSource().removeFeature( feature ); }, 10 );
	}
};

netgis.Map.prototype.onExportBegin = function( e )
{
	var params = e.detail;
	
	switch ( params.format )
	{
		case "geojson":
		{
			this.exportFeatures( params.nonEdits );
			break;
		}
		
		default:
		{
			this.exportImage( params.format, params.width, params.height, params.landscape, params.padding );
			break;
		}
	}
};

netgis.Map.prototype.onScalebarSelectChange = function( e )
{
	var scale = this.scalebarSelect.value;
	
	netgis.util.invoke( this.scalebarSelect, netgis.Events.MAP_ZOOM_SCALE, { scale: scale, anim: true } );
};

netgis.Map.prototype.onTimeSliderShow = function( e )
{
	var params = e.detail;
	
	//console.info( "Time Slider Show:", params );
	
	/*this.timesliderLayer = this.createLayerWMST( params.url, params.id );
	this.timesliderLayer.setZIndex( 30000 );
	
	this.map.addLayer( this.timesliderLayer );*/
};

netgis.Map.prototype.onTimeSliderHide = function( e )
{
	var params = e.detail;
	
	//console.info( "Time Slider Hide:", params );
	
	//this.map.removeLayer( this.timesliderLayer );
};

netgis.Map.prototype.onTimeSliderSelect = function( e )
{
	var params = e.detail;
	
	console.info( "Time Slider Select:", params );
	
	//this.timesliderLayer.getSource().updateParams( { "TIME": params.id } );
	
	this.layers[ params.layer ].getSource().updateParams( { "TIME": params.time } );
};