/* global ol, jsts, shp, proj4 */

"use strict";

var netgis = netgis || {};

/**
 * Map module implementation for OpenLayers 6+.
 */
netgis.MapOpenLayers = function()
{
	netgis.Map.call( this );
	
	this.mode = null;
	this.toolbars = {};
	this.view = null;
	this.map = null;
	this.layers = [];
	this.interactions = {};
	this.snap = null;
	this.snapFeatures = null;
	this.editLayer = null;
	this.parcelLayer = null;
	
	this.hover = null;
	this.selected = null;
	this.sketch = null;
	
	this.editEventsSilent = false;
	
	this.importLayerID = 20000;
	this.editLayerID = 30000;
	this.labelFont = "4mm Verdana, sans-serif";
	this.drawBufferRadius = 100.0;
	this.drawBufferSegments = 3;
};

netgis.MapOpenLayers.prototype = Object.create( netgis.Map.prototype );
netgis.MapOpenLayers.prototype.constructor = netgis.MapOpenLayers;

//TODO: not much benefits from Map class inheritance, may be dropped soon

netgis.MapOpenLayers.prototype.load = function()
{
	// Elements
	netgis.Map.prototype.load.call( this ); //TODO: ?
	
	this.dropTarget = document.createElement( "div" );
	this.dropTarget.className = "netgis-drop-target netgis-hide";
	this.dropTarget.innerHTML = "Datei hier ablegen!";
	this.root.appendChild( this.dropTarget );
	
	this.root.addEventListener( "dragenter", this.onDragEnter.bind( this ) );
	this.root.addEventListener( "dragover", this.onDragEnter.bind( this ) );
	this.root.addEventListener( "dragend", this.onDragLeave.bind( this ) );
	this.root.addEventListener( "dragleave", this.onDragLeave.bind( this ) );
	this.root.addEventListener( "drop", this.onDragDrop.bind( this ) );
	
	// Map Renderer
	this.initMap();
	this.initDefaultLayers();
	this.initInteractions();
	
	// Events
	this.client.on( netgis.Events.CONTEXT_UPDATE, this.onContextUpdate.bind( this ) );
	this.client.on( netgis.Events.MAP_UPDATE_STYLE, this.onUpdateStyle.bind( this ) );
	this.client.on( netgis.Events.EDIT_FEATURES_LOADED, this.onEditFeaturesLoaded.bind( this ) );
	this.client.on( netgis.Events.SET_MODE, this.onSetMode.bind( this ) );
	this.client.on( netgis.Events.SNAP_ON, this.onSnapOn.bind( this ) );
	this.client.on( netgis.Events.SNAP_OFF, this.onSnapOff.bind( this ) );
	this.client.on( netgis.Events.TRACING_ON, this.onTracingOn.bind( this ) );
	this.client.on( netgis.Events.TRACING_OFF, this.onTracingOff.bind( this ) );
	this.client.on( netgis.Events.LAYER_SHOW, this.onLayerShow.bind( this ) );
	this.client.on( netgis.Events.LAYER_HIDE, this.onLayerHide.bind( this ) );
	this.client.on( netgis.Events.MAP_ZOOM_WKT, this.onZoomWKT.bind( this ) );
	this.client.on( netgis.Events.MAP_SET_EXTENT, this.onSetExtent.bind( this ) );
	this.client.on( netgis.Events.MAP_CHANGE_ZOOM, this.onChangeZoom.bind( this ) );
	this.client.on( netgis.Events.BUFFER_CHANGE, this.onBufferChange.bind( this ) );
	this.client.on( netgis.Events.BUFFER_ACCEPT, this.onBufferAccept.bind( this ) );
	this.client.on( netgis.Events.BUFFER_CANCEL, this.onBufferCancel.bind( this ) );
	this.client.on( netgis.Events.IMPORT_GEOJSON, this.onImportGeoJSON.bind( this ) );
	this.client.on( netgis.Events.IMPORT_GML, this.onImportGML.bind( this ) );
	this.client.on( netgis.Events.IMPORT_SHAPEFILE, this.onImportShapefile.bind( this ) );
	this.client.on( netgis.Events.IMPORT_WKT, this.onImportWKT.bind( this ) );
	this.client.on( netgis.Events.IMPORT_SPATIALITE, this.onImportSpatialite.bind( this ) );
	this.client.on( netgis.Events.IMPORT_GEOPACKAGE, this.onImportGeopackage.bind( this ) );
	this.client.on( netgis.Events.EXPORT_PDF, this.onExportPDF.bind( this ) );
	this.client.on( netgis.Events.EXPORT_JPEG, this.onExportJPEG.bind( this ) );
	this.client.on( netgis.Events.EXPORT_PNG, this.onExportPNG.bind( this ) );
	this.client.on( netgis.Events.EXPORT_GIF, this.onExportGIF.bind( this ) );
	this.client.on( netgis.Events.PARCEL_SHOW_PREVIEW, this.onParcelShowPreview.bind( this ) );
	this.client.on( netgis.Events.PARCEL_HIDE_PREVIEW, this.onParcelHidePreview.bind( this ) );
	this.client.on( netgis.Events.ADD_SERVICE_WMS, this.onAddServiceWMS.bind( this ) );
	this.client.on( netgis.Events.ADD_SERVICE_WFS, this.onAddServiceWFS.bind( this ) );
	this.client.on( netgis.Events.DRAW_BUFFER_ON, this.onDrawBufferOn.bind( this ) );
	this.client.on( netgis.Events.DRAW_BUFFER_OFF, this.onDrawBufferOff.bind( this ) );
	this.client.on( netgis.Events.DRAW_BUFFER_RADIUS_CHANGE, this.onDrawBufferRadiusChange.bind( this ) );
	this.client.on( netgis.Events.DRAW_BUFFER_SEGMENTS_CHANGE, this.onDrawBufferSegmentsChange.bind( this ) );
};

netgis.MapOpenLayers.prototype.initMap = function()
{
	var config = this.client.config;
	
	// Projections ( WGS / Lon-Lat supported out of the box )
	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: config.map.projection,
		center: config.map.center,
		minZoom: config.map.minZoom,
		maxZoom: config.map.maxZoom,
		zoom: config.map.zoom
	};
	
	this.view = new ol.View
	(
		viewParams
	);

	// Map
	this.map = new ol.Map
	(
		{
			target: this.root,
			view: this.view,
			pixelRatio: 1.0,
			moveTolerance: 5,
			controls: []
		}
	);
	
	this.map.on( "pointermove", this.onPointerMove.bind( this ) );
	this.map.on( "click", this.onSingleClick.bind( this ) );
	
	this.map.on( "movestart", this.onMoveStart.bind( this ) );
	this.map.on( "moveend", this.onMoveEnd.bind( this ) );
	
	this.view.on( "change:resolution", this.onChangeResolution.bind( this ) );
};

netgis.MapOpenLayers.prototype.initDefaultLayers = function()
{
	//TODO: why id as z index ?
	
	this.editLayer = new ol.layer.Vector( { source: new ol.source.Vector( { features: [] } ), style: this.styleEdit.bind( this ), zIndex: this.editLayerID } );
	this.map.addLayer( this.editLayer );
	
	this.previewLayer = new ol.layer.Vector( { source: new ol.source.Vector( { features: [] } ), style: this.styleSketch.bind( this ), zIndex: this.editLayerID + 10 } );
	this.map.addLayer( this.previewLayer );
	
	this.parcelLayer = new ol.layer.Vector( { source: new ol.source.Vector( { features: [] } ), style: this.styleParcel.bind( this ), zIndex: this.editLayerID + 20 } );
	this.map.addLayer( this.parcelLayer );
	
	this.editEventsOn();
};

netgis.MapOpenLayers.prototype.editEventsOn = function()
{
	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 ) );
};

netgis.MapOpenLayers.prototype.editEventsOff = function()
{
	//NOTE: this doesn't work because OL does not allow removing all listeners and listener function ref is changed by binding
	//NOTE: see this.editEventsCommit
	
	//this.editLayer.getSource().un( "addfeature"/*, this.onEditLayerAdd*/ );
	//this.editLayer.getSource().un( "changefeature", this.onEditLayerChange.bind( this ) ); //TODO: fired on feature style change? use only one style function with selected/hover states?
	//this.editLayer.getSource().un( "removefeature"/*, this.onEditLayerRemove*/ );
};

netgis.MapOpenLayers.prototype.initInteractions = function()
{
	// View
	this.interactions[ netgis.Modes.VIEW ] =
	[
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.PANNING ] = this.interactions[ netgis.Modes.VIEW ];
	this.interactions[ netgis.Modes.ZOOMING_IN ] = this.interactions[ netgis.Modes.VIEW ];
	this.interactions[ netgis.Modes.ZOOMING_OUT ] = this.interactions[ netgis.Modes.VIEW ];
	
	// Draw
	this.interactions[ netgis.Modes.DRAW_POINTS ] =
	[
		new ol.interaction.Draw( { type: "Point", source: this.editLayer.getSource(), style: this.styleSketch.bind( this ) } ),
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	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 ) } ),
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	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 ) } ),
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	// Edit
	this.interactions[ netgis.Modes.CUT_FEATURE_BEGIN ] =
	[
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.CUT_FEATURE_DRAW ] =
	[
		new ol.interaction.Draw( { type: "Polygon" /*, source: this.editLayer.getSource()*/, style: this.styleSketch.bind( this ) } ),
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.CUT_FEATURE_DRAW ][ 0 ].on( "drawend", this.onCutFeatureDrawEnd.bind( this ) );
	
	this.interactions[ netgis.Modes.MODIFY_FEATURES ] =
	[
		new ol.interaction.Modify( { source: this.editLayer.getSource(), deleteCondition: ol.events.condition.doubleClick, style: this.styleModify.bind( this ) } ),
		//new ol.interaction.DragPan(),
		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.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.BUFFER_FEATURE_BEGIN ] =
	[
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.BUFFER_FEATURE_EDIT ] =
	[
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	// Snapping
	this.snapFeatures = new ol.Collection();
	
	// Search
	this.interactions[ netgis.Modes.SEARCH_PLACE ] =
	[
		new ol.interaction.DragPan(),
		new ol.interaction.MouseWheelZoom()
	];
	
	this.interactions[ netgis.Modes.SEARCH_PARCEL ] = this.interactions[ netgis.Modes.VIEW ];
};

netgis.MapOpenLayers.prototype.createLayer = function( data )
{
	var layer = null;
	
	// Create Specific Layer By Type
	switch ( data.type )
	{
		case netgis.LayerTypes.XYZ:
		{			
			layer = this.createLayerXYZ( data.url );
			break;
		}
		
		case netgis.LayerTypes.OSM:
		{
			layer = this.createLayerOSM();
			break;
		}
		
		case netgis.LayerTypes.WMS:
		{
			layer = this.createLayerWMS( data.url, data.name, data.format, data.username, data.password );
			break;
		}
		
		case netgis.LayerTypes.WFS:
		{
			layer = this.createLayerWFS( data.url, data.name, this.client.config.map.projection, data.outputFormat );
			break;
		}
	}
	
	// Common Settings
	if ( layer )
	{
		if ( data.minZoom ) layer.setMinZoom( Number.parseFloat( data.minZoom ) - 1.0 );
		if ( data.maxZoom ) layer.setMaxZoom( Number.parseFloat( data.maxZoom ) );
	}
	
	return layer;
};

netgis.MapOpenLayers.prototype.createLayerXYZ = function( url )
{
	var layer = new ol.layer.Tile
	(
		{
			source: new ol.source.XYZ
			(
				{
					url: url,
					crossOrigin: "anonymous"
				}
			)
		}
	);
	
	return layer;
};

netgis.MapOpenLayers.prototype.createLayerOSM = function()
{
	var layer = new ol.layer.Tile
	(
		{
			source: new ol.source.XYZ
			(
				{
					url: "https://{a-c}.tile.openstreetmap.de/{z}/{x}/{y}.png",
					crossOrigin: "anonymous"
				}
			)
		}
	);
	
	return layer;
};

netgis.MapOpenLayers.prototype.createLayerWMS = function( url, layerName, format, user, password )
{	
	var params =
	{
		url: url,
		params:
		{
			"LAYERS":		layerName,
			"FORMAT":		format ? format : "image/png",
			"TRANSPARENT":	"true",
			"VERSION":		"1.1.1"
		},
		serverType: "mapserver",
		crossOrigin: "anonymous",
		hidpi: false
		//ratio: 3.0
	};

	// Authorization
	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 = new ol.source.ImageWMS( params );

	var layer = new ol.layer.Image
	(
		{
			source:	source,
			//zIndex: index,
			opacity: 1.0
		}
	);
	
	return layer;
};

netgis.MapOpenLayers.prototype.createLayerWFS = function( url, typeName, projection, outputFormat )
{
	url = url
			+ "service=WFS"
			+ "&version=1.1.0"
			+ "&request=GetFeature";
			
	if ( ! outputFormat )
		outputFormat = "application/json";
	else
		outputFormat = netgis.util.replace( outputFormat, " ", "+" ); //TODO: encode uri component ?

	var source = new ol.source.Vector
	(
		{
			format: new ol.format.GeoJSON(),
			strategy: ol.loadingstrategy.bbox,
			url: function( extent )
			{
				return url
						+ "&typename=" + typeName
						+ "&srsname=" + projection
						+ "&bbox=" + extent.join( "," ) + "," + projection
						+ "&outputFormat=" + outputFormat;
			}
			
			//TODO: custom loader function to catch xhr response parse errors
		}
	);

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

	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.MapOpenLayers.prototype.clearAll = function()
{
	for ( var i = 0; i < this.layers.length; i++ )
	{
		this.map.removeLayer( this.layers[ i ] );
	}
	
	this.layers = [];
	
	this.snapFeatures.clear();
};

netgis.MapOpenLayers.prototype.onUpdateStyle = function( e )
{
	var style = new ol.style.Style
	(
		{
			//image: new ol.style.Circle( { radius: 7, fill: new ol.style.Fill( { color: "#ff0000" } ) } ),
			fill: new ol.style.Fill( { color: e.polygon.fill } ),
			stroke: new ol.style.Stroke( { color: e.polygon.stroke, width: e.polygon.strokeWidth } )
		}
	);
	
	this.editLayer.setStyle( style );
};

netgis.MapOpenLayers.prototype.styleEdit = function( feature )
{
	var radius = this.client.config.styles.editLayer.pointRadius;
	var geom = feature.getGeometry();
	
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: radius, fill: new ol.style.Fill( { color: this.client.config.styles.editLayer.stroke } ) } ),
			fill: new ol.style.Fill( { color: this.client.config.styles.editLayer.fill } ),
			stroke: new ol.style.Stroke( { color: this.client.config.styles.editLayer.stroke, width: this.client.config.styles.editLayer.strokeWidth } )
		}
	);
	
	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: this.labelFont,
					fill: new ol.style.Fill( { color: this.client.config.styles.editLayer.stroke } ),
					backgroundFill: new ol.style.Fill( { color: "rgba( 255, 255, 255, 0.5 )" } ),
					padding: [ 2, 4, 2, 4 ]
				}
			)
		);
	}
	
	return style;
};

netgis.MapOpenLayers.prototype.styleSelect = function( feature )
{
	var geom = feature.getGeometry();
	
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: this.client.config.styles.select.pointRadius, fill: new ol.style.Fill( { color: this.client.config.styles.select.stroke } ) } ),
			fill: new ol.style.Fill( { color: this.client.config.styles.select.fill } ),
			stroke: new ol.style.Stroke( { color: this.client.config.styles.select.stroke, width: this.client.config.styles.select.strokeWidth } )
		}
	);
	
	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: this.labelFont,
					fill: new ol.style.Fill( { color: this.client.config.styles.select.stroke } ),
					backgroundFill: new ol.style.Fill( { color: "rgba( 255, 255, 255, 0.5 )" } ),
					padding: [ 2, 4, 2, 4 ]
				}
			)
		);
	}
	
	return style;
};

netgis.MapOpenLayers.prototype.styleModify = function( feature )
{
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: this.client.config.styles.modify.pointRadius, fill: new ol.style.Fill( { color: this.client.config.styles.modify.stroke } ) } ),
			fill: new ol.style.Fill( { color: this.client.config.styles.modify.fill } ),
			stroke: new ol.style.Stroke( { color: this.client.config.styles.modify.stroke, width: this.client.config.styles.modify.strokeWidth } )
		}
	);
	
	var vertex = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: this.client.config.styles.modify.pointRadius, fill: new ol.style.Fill( { color: this.client.config.styles.modify.stroke } ) } ),
			geometry: this.getGeometryPoints( feature )
		}
	);
	
	return [ style, vertex ];
};

netgis.MapOpenLayers.prototype.styleSketch = function( feature )
{	
	var geom = feature.getGeometry();
	
	var style = new ol.style.Style
	(
		{
			image: new ol.style.Circle( { radius: this.client.config.styles.sketch.pointRadius, fill: new ol.style.Fill( { color: this.client.config.styles.sketch.stroke } ) } ),
			fill: new ol.style.Fill( { color: this.client.config.styles.sketch.fill } ),
			stroke: new ol.style.Stroke( { color: this.client.config.styles.sketch.stroke, width: this.client.config.styles.sketch.strokeWidth } )
		}
	);
	
	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: this.labelFont,
					fill: new ol.style.Fill( { color: this.client.config.styles.sketch.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: this.client.config.styles.sketch.pointRadius, fill: new ol.style.Fill( { color: this.client.config.styles.sketch.stroke } ) } ),
			geometry: this.getGeometryPoints( feature )
		}
	);
	
	return [ style, vertex ];
};

netgis.MapOpenLayers.prototype.styleParcel = function()
{
	//var radius = this.client.config.styles.editLayer.pointRadius;
	
	var style = new ol.style.Style
	(
		{
			//image: new ol.style.Circle( { radius: radius, fill: new ol.style.Fill( { color: this.client.config.styles.editLayer.stroke } ) } ),
			fill: new ol.style.Fill( { color: this.client.config.styles.parcel.fill } ),
			stroke: new ol.style.Stroke( { color: this.client.config.styles.parcel.stroke, width: this.client.config.styles.parcel.strokeWidth } )
		}
	);
	
	return style;
};

netgis.MapOpenLayers.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.MapOpenLayers.prototype.getActiveVectorLayers = function()
{
	var vectorLayers = [];
	var mapLayers = this.map.getLayers().getArray();
	
	var layers = this.layers; // this.map.getLayers().getArray();
	
	for ( var i = 0; i < layers.length; i++ )
	{
		//console.info( "Layer:", layers[ i ] );
		
		var layer = layers[ i ];
		
		if ( layer instanceof ol.layer.Vector && mapLayers.indexOf( layer ) > -1 )
		{
			vectorLayers.push( layer );
		}
	}
	
	return vectorLayers;
};

netgis.MapOpenLayers.prototype.setMode = function( mode )
{
	// Leave
	switch ( this.mode )
	{
		case netgis.Modes.BUFFER_FEATURE_EDIT:
		{
			this.onBufferCancel( null );
			break;
		}
		
		case netgis.Modes.DRAW_POINTS:
		case netgis.Modes.DRAW_LINES:
		{
			this.onDrawBufferOff( null );
			break;
		}
	}
	
	// Enter
	switch ( mode )
	{
	}
	
	// Interactions
	this.map.getInteractions().clear();
	
	var interactions = this.interactions[ mode ];
	
	if ( interactions )
	{
		for ( var i = 0; i < interactions.length; i++ )
		{
			this.map.addInteraction( interactions[ i ] );
		}
	}
	
	//TODO: set to default pan interactions when none found for mode ?
	
	if ( this.snap )
	{
		if ( mode === netgis.Modes.DRAW_POINTS || mode === netgis.Modes.DRAW_LINES || mode === netgis.Modes.DRAW_POLYGONS )
		{
			this.map.addInteraction( this.snap );
		}
	}
	
	// Style	
	switch ( mode )
	{
		default:
		{
			this.editLayer.setStyle( this.styleEdit.bind( this ) );
			break;
		}
		
		case netgis.Modes.MODIFY_FEATURES:
		{
			this.editLayer.setStyle( this.styleModify.bind( this ) );
			break;
		}
	};
	
	// Cursors
	if ( this.mode ) this.root.classList.remove( this.getModeClassName( this.mode ) );
	if ( mode ) this.root.classList.add( this.getModeClassName( mode ) );
	
	this.mode = mode;
};

netgis.MapOpenLayers.prototype.getModeClassName = function( mode )
{
	var modeClass = mode.toLowerCase();
	//modeClass = modeClass.replace( "_", "-" );
	modeClass = netgis.util.replace( modeClass, "_", "-" );
	modeClass = "netgis-mode-" + modeClass;
	
	return modeClass;
};

netgis.MapOpenLayers.prototype.setSnapOn = function()
{
	//this.snapFeatures = new ol.Collection();
	this.snap = new ol.interaction.Snap( { features: this.snapFeatures } );
	this.map.addInteraction( this.snap );
	
	this.snapFeatures.changed();
	
	//this.updateSnapLayers();
	
	//TODO: https://openlayers.org/en/latest/examples/tracing.html
};

netgis.MapOpenLayers.prototype.setSnapOff = function()
{
	if ( this.snap )
	{
		this.map.removeInteraction( this.snap );
		this.snap = null;
		//this.snapFeatures = null;
	}
};

netgis.MapOpenLayers.prototype.setTracingOn = function()
{
	var source = new ol.source.Vector( { features: this.snapFeatures } );
	
	this.tracing = new ol.interaction.Draw( { type: "Polygon", source: this.editLayer.getSource(), style: this.styleSketch.bind( this ), trace: true, traceSource: source } );

	var actions = this.interactions[ netgis.Modes.DRAW_POLYGONS ];
	actions[ 0 ].setActive( false );
	actions.push( this.tracing );
	
	this.setMode( this.mode );
};

netgis.MapOpenLayers.prototype.setTracingOff = function()
{
	var actions = this.interactions[ netgis.Modes.DRAW_POLYGONS ];
	actions[ 0 ].setActive( true );
	actions.splice( actions.indexOf( this.tracing ), 1 );
	
	this.setMode( this.mode );
};

/*
netgis.MapOpenLayers.prototype.updateSnapLayers = function()
{
	var snapLayers = this.getActiveVectorLayers();
	
	this.snapFeatures.clear();
	
	if ( snapLayers.length > 0 )
	{
		for ( var i = 0; i < snapLayers.length; i++ )
		{
			var layerFeatures = snapLayers[ i ].getSource().getFeatures();
			
			for ( var j = 0; j < layerFeatures.length; j++ )
			{
				this.snapFeatures.push( layerFeatures[ j ] );
			}
		}
		
		console.info( "Snap Features:", this.snapFeatures.getLength() );
	}
};
*/

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

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

netgis.MapOpenLayers.prototype.onSnapOn = function( e )
{
	this.setSnapOn();
};

netgis.MapOpenLayers.prototype.onSnapOff = function( e )
{
	this.setSnapOff();
};

netgis.MapOpenLayers.prototype.onTracingOn = function( e )
{
	this.setTracingOn();
};

netgis.MapOpenLayers.prototype.onTracingOff = function( e )
{
	this.setTracingOff();
};

netgis.MapOpenLayers.prototype.onLayerShow = function( e )
{
	var layer = this.layers[ e.id ];
	
	if ( ! layer ) return;
	
	this.map.addLayer( layer );
	
	//if ( /*this.snap &&*/ layer instanceof ol.layer.Vector ) this.addSnapLayer( layer ); //this.updateSnapLayers();
	
	if ( layer instanceof ol.layer.Vector ) this.addSnapLayer( layer );
};

netgis.MapOpenLayers.prototype.onLayerHide = function( e )
{
	var layer = this.layers[ e.id ];
	
	if ( ! layer ) return;
	
	this.map.removeLayer( layer );
	
	if ( layer instanceof ol.layer.Vector ) this.removeSnapLayer( layer ); //this.updateSnapLayers();
};

netgis.MapOpenLayers.prototype.onContextUpdate = function( e )
{
	this.clearAll();
	
	var context = e;
	
	// Bounding Box
	var bbox = context.bbox;
	
	if ( bbox )
	{
		var bbox1;
		var bbox2;
		
		if ( netgis.util.isDefined( this.client.config.map ) && netgis.util.isDefined( this.client.config.map.projection ) )
		{
			bbox1 = ol.proj.fromLonLat( [ bbox[ 0 ], bbox[ 1 ] ], this.client.config.map.projection );
			bbox2 = ol.proj.fromLonLat( [ bbox[ 2 ], bbox[ 3 ] ], this.client.config.map.projection );
		}
		else
		{
			bbox1 = ol.proj.fromLonLat( [ bbox[ 0 ], bbox[ 1 ] ] );
			bbox2 = ol.proj.fromLonLat( [ bbox[ 2 ], bbox[ 3 ] ] );
		}

		bbox[ 0 ] = bbox1[ 0 ];
		bbox[ 1 ] = bbox1[ 1 ];
		bbox[ 2 ] = bbox2[ 0 ];
		bbox[ 3 ] = bbox2[ 1 ];

		this.view.fit( bbox );
	}
	
	// Layers
	//this.layers = [];
	
	for ( var l = 0; l < context.layers.length; l++ )
	//for ( var l = context.layers.length - 1; l >= 0; l-- )
	{
		var data = context.layers[ l ];
		var layer = this.createLayer( data );
		
		if ( layer )
		{
			layer.setZIndex( context.layers.length - l );
			//this.layers.push( layer );
			this.layers[ l ] = layer;
		}
	}
	
	//this.map.getLayers().clear();
	
	//TODO: active layers from context?
	
	// Active State
	/*for ( var l = 0; l < context.layers.length; l++ )
	{
		var data = context.layers[ l ];
		if ( data.active ) this.onLayerShow( { id: l } );
	}*/
};

netgis.MapOpenLayers.prototype.onAddServiceWMS = function( e )
{
	var layer = this.createLayerWMS( e.url, e.name, e.format );
	layer.setZIndex( e.id );
	this.layers[ e.id ] = layer;
};

netgis.MapOpenLayers.prototype.onAddServiceWFS = function( e )
{
	var layer = this.createLayerWFS( e.url, e.name, this.client.config.map.projection, e.format );
	layer.setZIndex( e.id );
	this.layers[ e.id ] = layer;
};

netgis.MapOpenLayers.prototype.onSetMode = function( e )
{
	this.setMode( e );
};

netgis.MapOpenLayers.prototype.onSetExtent = function( e )
{
	var minxy = ol.proj.fromLonLat( [ e.minx, e.miny ], this.client.config.map.projection );
	var maxxy = ol.proj.fromLonLat( [ e.maxx, e.maxy ], this.client.config.map.projection );
	
	this.view.fit( [ minxy[ 0 ], minxy[ 1 ], maxxy[ 0 ], maxxy[ 1 ] ] );
};

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

netgis.MapOpenLayers.prototype.onZoomWKT = function( e )
{
	var parser = new ol.format.WKT();
	var geom = parser.readGeometry( e );
	var padding = 40;
	
	this.view.fit( geom, { duration: 300, padding: [ padding, padding, padding, padding ] } );
	
	//TODO: take visible panels into account when zooming
};

netgis.MapOpenLayers.prototype.onPointerMove = function( e )
{
	var pixel = e.pixel;
	var coords = e.coordinate;
	
	var hover = this.hover;
	var styleSelect = this.styleSelect.bind( this );
	
	if ( hover )
	{
		hover.setStyle( this.styleEdit.bind( this ) );
		hover = null;
	}
	
	var self = this;
	
	switch ( this.mode )
	{
		case netgis.Modes.DELETE_FEATURES:
		{
			this.map.forEachFeatureAtPixel
			(
				pixel,
				function( feature, layer ) //TODO: bind to this?
				{
					if ( layer === self.editLayer )
					{
						hover = feature;
						feature.setStyle( styleSelect );
					}
					
					return true;
				}
			);
			
			break;
		}
		
		case netgis.Modes.CUT_FEATURE_BEGIN:
		{
			this.map.forEachFeatureAtPixel
			(
				pixel,
				function( feature, layer ) //TODO: bind to this?
				{
					if ( layer === self.editLayer )
					{
						hover = feature;
						feature.setStyle( styleSelect );
					}
					
					return true;
				}
			);
			
			break;
		}
		
		case netgis.Modes.BUFFER_FEATURE_BEGIN:
		{
			this.map.forEachFeatureAtPixel
			(
				pixel,
				function( feature, layer ) //TODO: bind to this?
				{
					if ( layer === self.editLayer )
					{
						hover = feature;
						feature.setStyle( styleSelect );
					}
					
					return true;
				}
			);
			
			break;
		}
		
		case netgis.Modes.DRAW_POINTS:
		case netgis.Modes.DRAW_LINES:
		{
			this.updateDrawBufferPreview();
			break;
		}
	}
	
	//TODO: refactor to default hover handler?
	
	this.hover = hover;
};

netgis.MapOpenLayers.prototype.onSingleClick = function( e )
{
	switch ( this.mode )
	{
		case netgis.Modes.DELETE_FEATURES:
		{
			if ( this.hover )
			{
				this.editLayer.getSource().removeFeature( this.hover );
				this.hover = null;
			}
			
			break;
		}
		
		case netgis.Modes.CUT_FEATURE_BEGIN:
		{
			if ( this.hover )
			{
				this.selected = this.hover;
				this.client.invoke( netgis.Events.SET_MODE, netgis.Modes.CUT_FEATURE_DRAW );
			}
			
			break;
		}
		
		case netgis.Modes.BUFFER_FEATURE_BEGIN:
		{
			if ( this.hover )
			{
				this.selected = this.hover;
				this.client.invoke( netgis.Events.SET_MODE, netgis.Modes.BUFFER_FEATURE_EDIT );
			}
			
			break;
		}
	}
};

netgis.MapOpenLayers.prototype.onMoveStart = function( e )
{
	//TODO: problem with toolbars after head menu click
	//this.client.invoke( netgis.Events.MAP_SET_MODE, netgis.MapModes.PANNING );
};

netgis.MapOpenLayers.prototype.onMoveEnd = function( e )
{
	//TODO: problem with toolbars after head menu click
	//this.client.invoke( netgis.Events.MAP_SET_MODE, netgis.MapModes.VIEW );
};

netgis.MapOpenLayers.prototype.onChangeResolution = function( e )
{
	//var d = e.oldValue - this.view.getResolution();
	//this.client.invoke( netgis.Events.MAP_SET_MODE, ( d > 0.0 ) ? netgis.MapModes.ZOOMING_IN : netgis.MapModes.ZOOMING_OUT );
};

netgis.MapOpenLayers.prototype.onCutFeatureDrawEnd = function( e )
{
	var cutter = e.feature;
	var target = this.selected;
	
	if ( target )
	{
		// 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.selected = feature;
	}
	
	this.editEventsSilent = true;
	this.splitMultiPolygons( this.editLayer );
	this.editEventsSilent = false;
	this.updateEditOutput();
};

netgis.MapOpenLayers.prototype.onModifyFeaturesEnd = function( e )
{
	this.updateEditOutput();
	this.updateEditArea();
};

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

netgis.MapOpenLayers.prototype.createBufferGeometry = function( srcgeom, radius, segments )
{
	var parser = new jsts.io.OL3Parser();
		
	var a = parser.read( srcgeom );
	var b = a.buffer( radius, segments );
	
	var geom = parser.write( b );
	
	return geom;
};

netgis.MapOpenLayers.prototype.onBufferChange = function( e )
{
	var source = this.editLayer.getSource();
	var target = this.selected;
	
	if ( this.sketch )
	{
		source.removeFeature( this.sketch );
	}
	
	if ( target )
	{
		var feature = this.createBufferFeature( target.getGeometry(), e.radius, e.segments );
		
		//source.removeFeature( target );
		source.addFeature( feature );
		
		this.sketch = feature;
	}
};

netgis.MapOpenLayers.prototype.onBufferAccept = function( e )
{
	if ( this.selected && this.sketch )
	{
		var source = this.editLayer.getSource();
		
		// Delete Input Feature
		//if ( ! ( this.selected.getGeometry() instanceof ol.geom.Point ) )
			source.removeFeature( this.selected );
	}
	
	this.sketch = null;
	this.selected = null;
};

netgis.MapOpenLayers.prototype.onBufferCancel = function( e )
{
	if ( this.sketch )
	{
		this.editLayer.getSource().removeFeature( this.sketch );
		this.sketch = null;
	}
	
	this.selected = null;
};

netgis.MapOpenLayers.prototype.onDrawPointsEnd = function( e )
{
	var preview = this.previewLayer.getSource().getFeatures()[ 0 ];
	
	if ( preview )
	{
		var src = this.editLayer.getSource();
		src.addFeature( preview.clone() );
		
		//TODO: remove sketch point ?
		//this.editLayer.getSource().removeFeature( e.feature );
		
		/*window.setTimeout( function() {
		var features = src.getFeatures();
		src.removeFeature( features[ features.length - 1 ] );
		src.addFeature( preview.clone() );
		}, 10 );*/
		
		/*e.preventDefault();
		e.stopPropagation();
		return false;*/
	}
};

netgis.MapOpenLayers.prototype.onDrawLinesEnd = function( e )
{
	var preview = this.previewLayer.getSource().getFeatures()[ 0 ];
	if ( ! preview ) return;
	
	var src = this.editLayer.getSource();
	src.addFeature( preview.clone() );
};

netgis.MapOpenLayers.prototype.onDrawBufferOn = function( e )
{
	var feature = this.createBufferFeature( new ol.geom.Point( this.client.config.map.center ), this.drawBufferRadius, this.drawBufferSegments );
	this.previewLayer.getSource().addFeature( feature );
	
	//TODO: send all draw buffer params with events ?
};

netgis.MapOpenLayers.prototype.onDrawBufferOff = function( e )
{
	this.previewLayer.getSource().clear();
};

netgis.MapOpenLayers.prototype.onDrawBufferRadiusChange = function( e )
{
	var radius = e;
	this.drawBufferRadius = radius;
	
	this.updateDrawBufferPreview();
};

netgis.MapOpenLayers.prototype.onDrawBufferSegmentsChange = function( e )
{
	var segs = e;
	this.drawBufferSegments = segs;
	
	this.updateDrawBufferPreview();
};

netgis.MapOpenLayers.prototype.updateDrawBufferPreview = function()
{
	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.MapOpenLayers.prototype.onEditLayerAdd = function( e )
{
	////this.updateEditOutput();
	this.updateEditLayerItem();
	this.updateEditOutput();
	this.snapFeatures.push( e.feature );
};

netgis.MapOpenLayers.prototype.onEditLayerRemove = function( e )
{
	this.updateEditOutput();
	this.snapFeatures.remove( e.feature );
};

netgis.MapOpenLayers.prototype.onEditLayerChange = function( e )
{
	this.updateEditOutput();
};

netgis.MapOpenLayers.prototype.updateEditOutput = function()
{
	var features = this.editLayer.getSource().getFeatures();
	
	var proj = this.client.config.map.projection;
	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;
	
	if ( ! this.editEventsSilent )
	{
		this.client.invoke( netgis.Events.EDIT_FEATURES_CHANGE, output );
	}
};

netgis.MapOpenLayers.prototype.updateEditLayerItem = function()
{
	// Create layer item if not existing
	var id = this.editLayerID;
	
	if ( ! this.layers[ id ] )
	{
		this.layers[ id ] = this.editLayer;
		this.client.invoke( netgis.Events.LAYER_CREATED, { id: id, title: "Zeichnung", checked: true, folder: "draw" } );
	}
};

netgis.MapOpenLayers.prototype.updateEditArea = function()
{
	
};

netgis.MapOpenLayers.prototype.onEditFeaturesLoaded = function( e )
{
	var json = e;	
	var self = this;
	window.setTimeout( function() { self.createLayerGeoJSON( "Import", json ); }, 10 );
};

netgis.MapOpenLayers.prototype.onDragEnter = function( e )
{
	e.preventDefault();
	
	this.dropTarget.classList.remove( "netgis-hide" );
	
	//TODO: refactor into dragdrop module + events ?
	
	return false;
};

netgis.MapOpenLayers.prototype.onDragLeave = function( e )
{
	this.dropTarget.classList.add( "netgis-hide" );
	
	return false;
};

netgis.MapOpenLayers.prototype.onDragDrop = function( e )
{
	console.info( "Drag Drop" );
	
	this.dropTarget.classList.add( "netgis-hide" );
	
	e.preventDefault();
		
	var file = e.dataTransfer.files[ 0 ];
	var reader = new FileReader();

	reader.onload = this.onDragLoad.bind( this );

	console.log( "File:", file );

	//reader.readAsDataURL( file );
	reader.readAsArrayBuffer( file );

	return false;
};

netgis.MapOpenLayers.prototype.onDragLoad = function( e )
{
	console.log( "On Load:", e.target );
	
	this.loadShape( e.target.result );
};

netgis.MapOpenLayers.prototype.loadShape = function( data )
{
	var self = this;
	
	shp( data ).then
	(
		function( geojson )
		{
			self.onShapeLoad( geojson );
		}
	);
};

netgis.MapOpenLayers.prototype.onShapeLoad = function( geojson )
{
	console.info( "Shapefile To Geojson:", geojson );
			
	var features = new ol.format.GeoJSON( { dataProjection: "EPSG:4326", featureProjection: "EPSG:3857" } ).readFeatures( geojson );
	this.importLayer.getSource().addFeatures( features );

	this.view.fit( this.importLayer.getSource().getExtent(), {} );
};

netgis.MapOpenLayers.prototype.onImportGeoJSON = function( e )
{
	var file = e;
	var title = file.name;
	var self = this;
	
	var reader = new FileReader();
	reader.onload = function( e ) { self.createLayerGeoJSON( title, e.target.result ); };
	reader.readAsText( file );
};

netgis.MapOpenLayers.prototype.onImportGML = function( e )
{
	var file = e;
	var title = file.name;
	var self = this;
	
	var reader = new FileReader();
	reader.onload = function( e ) { self.createLayerGML( title, e.target.result ); };
	reader.readAsText( file );
};

netgis.MapOpenLayers.prototype.onImportShapefile = function( e )
{
	var file = e;
	var title = file.name;
	var self = this;
	
	var reader = new FileReader();
	reader.onload = function( e ) { self.createLayerShapefile( title, e.target.result ); };
	reader.readAsArrayBuffer( file );
};

netgis.MapOpenLayers.prototype.onImportSpatialite = function( e )
{
	var file = e;
	var title = file.name;
	var self = this;
	
	var reader = new FileReader();
	reader.onload = function( e ) { self.createLayerSpatialite( title, e.target.result ); };
	reader.readAsArrayBuffer( file );
};

netgis.MapOpenLayers.prototype.onImportGeopackage = function( e )
{
	var file = e;
	var title = file.name;
	var self = this;
	
	var reader = new FileReader();
	reader.onload = function( e ) { self.createLayerGeopackage( title, e.target.result ); };
	reader.readAsArrayBuffer( file );
};

netgis.MapOpenLayers.prototype.createLayerGeoJSON = function( title, data )
{	
	var format = new ol.format.GeoJSON();
	var projection = format.readProjection( data );
	var features = format.readFeatures( data, { featureProjection: this.client.config.map.projection } );

	//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.client.config.map.projection:
		{
			// Projection OK
			break;
		}
		
		default:
		{
			// Projection Not Supported
			console.warn( "Unsupported Import Projection:", projcode );
			break;
		}
	}	

	this.addImportedFeatures( features );
};

netgis.MapOpenLayers.prototype.createLayerGML = function( title, 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 format = new ol.format.GML3( { srsName: "EPSG::25832", featureType: "Test", featureNS: "http://www.opengis.net/gml" } );
	//var format = new ol.format.GML( { featureNS: "ogr" } );
	//var format = new ol.format.WFS( /*{ srsName: "EPSG:4326", featureType: "ogr:RLP_OG_utf8_epsg4326" }*/ );
	//var format = new ol.format.GML( { featureNS: "ogr", featureType: "ogr:RLP_OG_utf8_epsg4326" } );
	//var format = new ol.format.WFS();
	//var format = new ol.format.WFS( { featureNS: "ogr", featureType: "RLP_OG_utf8_epsg4326" } );
	//var projection = format.readProjection( data );
	//var features = format.readFeatures( data, { dataProjection: "EPSG:4326", featureProjection: "EPSG:3857" } );
	
	//var features = format.readFeatures( data, { dataProjection: this.client.config.map.projection, featureProjection: this.client.config.map.projection } );

	//console.info( "GML:", projection, features, features[ 0 ].getGeometry() );

	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.client.config.map.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 );
};

netgis.MapOpenLayers.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.MapOpenLayers.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.MapOpenLayers.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.client.config.map.projection );
	}
	
	return coords;
};

netgis.MapOpenLayers.prototype.createLayerShapefile = function( title, shapeData )
{
	var self = this;
	
	shp( shapeData ).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.client.config.map.projection } );
			
			self.addImportedFeatures( features );
		}
	);
};

netgis.MapOpenLayers.prototype.createLayerSpatialite = function( title, data )
{
	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 '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; }
				}

				// 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.client.config.map.projection } );

					features.push( new ol.Feature( { geometry: geom } ) );
				}
			}
			
			self.addImportedFeatures( features );
		}
	);
};

netgis.MapOpenLayers.prototype.createLayerGeopackage = function( title, data )
{
	var self = this;
	var arr = new Uint8Array( data );
	
	window.GeoPackage.setSqljsWasmLocateFile( function( file ) { return self.client.config.import.geopackageLibURL + 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.client.config.map.projection } );
				var feature = new ol.Feature( { geometry: geom } );
				features.push( feature );
			}
		}
		
		self.addImportedFeatures( features );
	} );
};

netgis.MapOpenLayers.prototype.addImportedFeatures = function( features )
{
	// Add To Edit Layer
	this.editEventsSilent = true;
	this.editLayer.getSource().addFeatures( features );
	this.editEventsSilent = false;
	this.updateEditOutput();
	
	// Zoom Imported Features
	if ( features.length > 0 )
	{
		var extent = features[ 0 ].getGeometry().getExtent();
		
		for ( var f = 1; f < features.length; f++ )
		{
			ol.extent.extend( extent, features[ f ].getGeometry().getExtent() );
		}
		
		var padding = 40;
		this.view.fit( extent, { duration: 300, padding: [ padding, padding, padding, padding ] } );
	}
};

netgis.MapOpenLayers.prototype.onImportWKT = function( e )
{
	var parser = new ol.format.WKT();
	var geom = parser.readGeometry( e );
	var feature = new ol.Feature( { geometry: geom } );
	
	this.addImportedFeatures( [ feature ] );
};

netgis.MapOpenLayers.prototype.onExportPDF = function( e )
{
	this.exportImage( "pdf", e.resx, e.resy, e.mode, e.margin );
};

netgis.MapOpenLayers.prototype.onExportJPEG = function( e )
{
	this.exportImage( "jpeg", e.resx, e.resy );
};

netgis.MapOpenLayers.prototype.onExportPNG = function( e )
{
	this.exportImage( "png", e.resx, e.resy );
};

netgis.MapOpenLayers.prototype.onExportGIF = function( e )
{
	this.exportImage( "gif", e.resx, e.resy );
};

netgis.MapOpenLayers.prototype.onParcelShowPreview = function( e )
{
	var parser = new ol.format.WKT();
	var geom = parser.readGeometry( e.geom );
	var feature = new ol.Feature( { geometry: geom } );
	
	this.parcelLayer.getSource().clear();
	this.parcelLayer.getSource().addFeature( feature );
};

netgis.MapOpenLayers.prototype.onParcelHidePreview = function( e )
{
	this.parcelLayer.getSource().clear();
};

netgis.MapOpenLayers.prototype.getWidth = function()
{
	return this.map.getSize()[ 0 ];
};

netgis.MapOpenLayers.prototype.getHeight = function()
{
	return this.map.getSize()[ 1 ];
};

/**
* 
* @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.MapOpenLayers.prototype.exportImage = function( format, resx, resy, mode, margin )
{
	this.client.invoke( netgis.Events.EXPORT_BEGIN, null );
	
	var self = this;
	var root = this.root;
	var map = this.map;
	var config = this.client.config;
	
	// 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.export.defaultFilename + ".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.export.defaultFilename + ".jpg" );
						}
						else
						{
							link.setAttribute( "download", config.export.defaultFilename + ".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.export.defaultFilename + ".png" );
							/*else
								window.open( mapCanvas.msToBlob(), "_blank" );*/
						}
						else
						{
							/*if ( ! config.export.openNewTab )
							{*/
								link.setAttribute( "download", config.export.defaultFilename + ".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.export.defaultFilename + ".gif" );
						
						var gif = new GIF( { workerScript: config.export.gifWebWorker, 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 );
				
				self.client.invoke( netgis.Events.EXPORT_END, null );
			}
		);

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

netgis.MapOpenLayers.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 );
};