Explorar o código

Campo para formulario de mapa

Guillermo Espinoza %!s(int64=8) %!d(string=hai) anos
pai
achega
b5c64633d8

+ 31 - 0
Form/Type/LeafletMapType.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace LeafletBundle\Form\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
+
+class LeafletMapType extends AbstractType
+{
+
+    /**
+     * @param OptionsResolver $resolver
+     */
+    public function configureOptions(OptionsResolver $resolver)
+    {
+        $resolver->setDefaults(array(
+            'mapped' => false,
+            'required' => false,
+        ));
+    }
+
+    /**
+     * @return string
+     */
+    public function getParent()
+    {
+        return HiddenType::class;
+    }
+
+}

BIN=BIN
Resources/public/images/loader.gif


BIN=BIN
Resources/public/images/search-icon-mobile.png


BIN=BIN
Resources/public/images/search-icon.png


+ 75 - 0
Resources/public/js/leaflet-map-widget.js

@@ -0,0 +1,75 @@
+var geocoder;
+
+$(document).ready(function () {
+    geocoder = new google.maps.Geocoder();
+
+    map.addControl(new L.Control.Search({
+        sourceData: googleGeocoding,
+        formatData: formatJSON,
+        markerLocation: true,
+        autoType: false,
+        autoCollapse: true,
+        minLength: 2,
+        position: 'topright'
+    }));
+
+    $('.search-button').on('click', function () {
+        if (loc) {
+            drawMarker();
+            setDataValue();
+        }
+    });
+
+    $('.search-input').on('keypress', function (e) {
+        if (e.keyCode == 13 && loc) {
+            e.preventDefault();
+            e.stopPropagation();
+            drawMarker();
+            setDataValue();
+        }
+    });
+
+    map.on('zoomend', function () {
+        setDataValue();
+    });
+
+    map.on('click', function (e) {
+        loc = new L.latLng(e.latlng);
+        drawMarker();
+        setDataValue();
+    });
+});
+function setDataValue()
+{
+    var jsonParseData = {};
+    var dataValue = $('[name$="[data]"]').val();
+    if (dataValue) {
+        jsonParseData = JSON.parse(dataValue);
+    }
+    jsonParseData.lat = marker._latlng.lat;
+    jsonParseData.lng = marker._latlng.lng;
+    jsonParseData.zoom = map.getZoom();
+
+    var stringifyData = JSON.stringify(jsonParseData);
+
+    $('[name$="[data]"]').val(stringifyData);
+    $('[name$="[map]"]').val(stringifyData);
+}
+
+function googleGeocoding(text, callResponse)
+{
+    geocoder.geocode({address: text}, callResponse);
+}
+
+function formatJSON(rawjson)
+{
+    var json = {},
+            key;
+    for (var i in rawjson) {
+        key = rawjson[i].formatted_address;
+        loc = new L.latLng(rawjson[i].geometry.location.lat(), rawjson[i].geometry.location.lng());
+        json[key] = loc;
+    }
+
+    return json;
+}

+ 80 - 0
Resources/public/js/leaflet-map.js

@@ -0,0 +1,80 @@
+window.SONATA_CONFIG.USE_ICHECK = false;
+
+var map,
+defaultCoords,
+drawnItems,
+marker,
+loc;
+
+$(document).ready(function () {
+    defaultCoords = new L.latLng(39.791312, -2.6949709); // España
+    map = new L.Map('map', {zoom: getZoomLevel(), center: getDataCoords()});
+    drawnItems = L.featureGroup().addTo(map);
+
+    drawMarker();
+
+    L.control.layers(
+            {
+                'openstreetmap': L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+                        {
+                            maxZoom: 18,
+                            attribution: '&copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors'
+                        }).addTo(map),
+                "google": L.tileLayer('http://www.google.com.ar/maps/vt?lyrs=y@189&gl=cn&x={x}&y={y}&z={z}',
+                        {
+                            maxZoom: 18,
+                            attribution: 'google'
+                        })
+            },
+            {
+                'drawlayer': drawnItems
+            },
+            {
+                position: 'topleft',
+                collapsed: false
+            }).addTo(map);
+});
+
+function getDataCoords()
+{
+    var jsonParseData = {};
+    if ($('[name$="[data]"]').val()) {
+        dataValue = $('[name$="[data]"]').val();
+    }
+    if (dataValue) {
+        jsonParseData = JSON.parse(dataValue);
+    }
+    loc = defaultCoords;
+    if (jsonParseData.lat && jsonParseData.lng) {
+        loc = new L.latLng(jsonParseData.lat, jsonParseData.lng);
+    }
+
+    return loc;
+}
+
+function drawMarker()
+{
+    if (marker) {
+        drawnItems.removeLayer(marker);
+    }
+    map.panTo(loc);
+    marker = L.marker([loc.lat, loc.lng]);
+    drawnItems.addLayer(marker);
+}
+
+function getZoomLevel()
+{
+    var jsonParseData = {};
+    var zoomLevel = 18;
+    if ($('[name$="[data]"]').val()) {
+        dataValue = $('[name$="[data]"]').val();
+    }
+    if (dataValue) {
+        jsonParseData = JSON.parse(dataValue);
+    }
+    if (jsonParseData.zoom) {
+        zoomLevel = jsonParseData.zoom;
+    }
+
+    return zoomLevel;
+}

+ 45 - 0
Resources/public/leaflet/leaflet-search-geocoder.js

@@ -0,0 +1,45 @@
+
+(function (factory) {
+    if(typeof define === 'function' && define.amd) {
+    //AMD
+        define(['leaflet'], factory);
+    } else if(typeof module !== 'undefined') {
+    // Node/CommonJS
+        module.exports = factory(require('leaflet'));
+    } else {
+    // Browser globals
+        if(typeof window.L === 'undefined')
+            throw 'Leaflet must be loaded first';
+        factory(window.L);
+    }
+})(function (L) {
+
+L.Control.Search.include({
+	options: {
+		geocoder: 'google',
+		markerLocation: true,
+		autoType: false,
+		autoCollapse: true,
+		minLength: 2
+	},
+/*	onAdd: function (map) {
+		L.Control.Search.prototype.onAdd.call(this, map);
+		console.log('Geocoder',this.options)
+	},*/
+	geocoders: {
+/*		'google': {
+			url: "//maps.googleapis.com/maps/api/geocode/json?key={key}&address={text}"
+		},
+		'nominatim': {
+			    
+
+	      format: 'json',
+	      q: query,
+	    });
+
+    		"//nominatim.openstreetmap.org/search?"
+		}*/
+	}
+});
+
+});

+ 118 - 0
Resources/public/leaflet/leaflet-search.css

@@ -0,0 +1,118 @@
+ 
+.leaflet-container .leaflet-control-search {
+	position:relative;
+	float:left;
+	background:#fff;
+	color:#1978cf;
+	-moz-border-radius: 4px;
+	-webkit-border-radius: 4px;
+	border-radius: 4px;
+	background-color: rgba(255, 255, 255, 0.8);
+	z-index:1000;	
+	box-shadow: 0 1px 7px rgba(0,0,0,0.65);
+	margin-left: 10px;
+	margin-top: 10px;
+}
+.leaflet-control-search.search-exp {/*expanded*/
+	box-shadow: 0 1px 7px #999;
+	background: #fff;
+}
+.leaflet-control-search .search-input {
+	display:block;
+	float:left;
+	background: #fff;
+	border:1px solid #666;
+	border-radius:2px;
+	height:18px;
+	padding:0 18px 0 2px;
+	margin:3px 0 3px 3px;
+}
+.leaflet-control-search.search-load .search-input {
+	background: url('../images/loader.gif') no-repeat center right #fff;
+}
+.leaflet-control-search.search-load .search-cancel {
+	visibility:hidden;
+}
+.leaflet-control-search .search-cancel {
+	display:block;
+	width:22px;
+	height:18px;
+	position:absolute;
+	right:22px;
+	margin:3px 0;
+	background: url('../images/search-icon.png') no-repeat 0 -46px;
+	text-decoration:none;
+	filter: alpha(opacity=80);
+	opacity: 0.8;		
+}
+.leaflet-control-search .search-cancel:hover {
+	filter: alpha(opacity=100);
+	opacity: 1;
+}
+.leaflet-control-search .search-cancel span {
+	display:none;/* comment for cancel button imageless */
+	font-size:18px;
+	line-height:20px;
+	color:#ccc;
+	font-weight:bold;
+}
+.leaflet-control-search .search-cancel:hover span {
+	color:#aaa;
+}
+.leaflet-control-search .search-button {
+	display:block;
+	float:left;
+	width:26px;
+	height:26px;	
+	background: url('../images/search-icon.png') no-repeat 2px 2px #fff;
+	border-radius:4px;
+}
+.leaflet-control-search .search-button:hover {
+	background: url('../images/search-icon.png') no-repeat 2px -22px #fafafa;
+}
+.leaflet-control-search .search-tooltip {
+	position:absolute;
+	top:100%;
+	left:0;
+	float:left;
+	list-style: none;
+	padding-left: 0;
+	min-width:120px;
+	max-height:122px;
+	box-shadow: 1px 1px 6px rgba(0,0,0,0.4);
+	background-color: rgba(0, 0, 0, 0.25);
+	z-index:1010;
+	overflow-y:auto;
+	overflow-x:hidden;
+	cursor: pointer;
+}
+.leaflet-control-search .search-tip {
+	margin:2px;
+	padding:2px 4px;
+	display:block;
+	color:black;
+	background: #eee;
+	border-radius:.25em;
+	text-decoration:none;	
+	white-space:nowrap;
+	vertical-align:center;
+}
+.leaflet-control-search .search-button:hover {
+	background-color: #f4f4f4;
+}
+.leaflet-control-search .search-tip-select,
+.leaflet-control-search .search-tip:hover {
+	background-color: #fff;
+}
+.leaflet-control-search .search-alert {
+	cursor:pointer;
+	clear:both;
+	font-size:.75em;
+	margin-bottom:5px;
+	padding:0 .25em;
+	color:#e00;
+	font-weight:bold;
+	border-radius:.25em;
+}
+
+

+ 987 - 0
Resources/public/leaflet/leaflet-search.src.js

@@ -0,0 +1,987 @@
+/* 
+ * Leaflet Control Search v2.7.2 - 2017-04-08 
+ * 
+ * Copyright 2017 Stefano Cudini 
+ * stefano.cudini@gmail.com 
+ * http://labs.easyblog.it/ 
+ * 
+ * Licensed under the MIT license. 
+ * 
+ * Demo: 
+ * http://labs.easyblog.it/maps/leaflet-search/ 
+ * 
+ * Source: 
+ * git@github.com:stefanocudini/leaflet-search.git 
+ * 
+ */
+(function (factory) {
+    if(typeof define === 'function' && define.amd) {
+    //AMD
+        define(['leaflet'], factory);
+    } else if(typeof module !== 'undefined') {
+    // Node/CommonJS
+        module.exports = factory(require('leaflet'));
+    } else {
+    // Browser globals
+        if(typeof window.L === 'undefined')
+            throw 'Leaflet must be loaded first';
+        factory(window.L);
+    }
+})(function (L) {
+
+	function _getPath(obj, prop) {
+		var parts = prop.split('.'),
+			last = parts.pop(),
+			len = parts.length,
+			cur = parts[0],
+			i = 1;
+
+		if(len > 0)
+			while((obj = obj[cur]) && i < len)
+				cur = parts[i++];
+
+		if(obj)
+			return obj[last];
+	}
+
+	function _isObject(obj) {
+		return Object.prototype.toString.call(obj) === "[object Object]";
+	}
+
+//TODO implement can do research on multiple sources layers and remote		
+//TODO history: false,		//show latest searches in tooltip		
+//FIXME option condition problem {autoCollapse: true, markerLocation: true} not show location
+//FIXME option condition problem {autoCollapse: false }
+//
+//TODO here insert function that search inputText FIRST in _recordsCache keys and if not find results.. 
+//  run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip
+//
+//TODO change structure of _recordsCache
+//	like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...}
+//	in this mode every record can have a free structure of attributes, only 'loc' is required
+//TODO important optimization!!! always append data in this._recordsCache
+//  now _recordsCache content is emptied and replaced with new data founded
+//  always appending data on _recordsCache give the possibility of caching ajax, jsonp and layersearch!
+//
+//TODO here insert function that search inputText FIRST in _recordsCache keys and if not find results.. 
+//  run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip
+//
+//TODO change structure of _recordsCache
+//	like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...}
+//	in this way every record can have a free structure of attributes, only 'loc' is required
+
+L.Control.Search = L.Control.extend({
+	includes: L.Mixin.Events,
+	//
+	//	Name					Data passed			   Description
+	//
+	//Managed Events:
+	//	search:locationfound	{latlng, title, layer} fired after moved and show markerLocation
+	//	search:expanded			{}					   fired after control was expanded
+	//  search:collapsed		{}					   fired after control was collapsed
+	//
+	//Public methods:
+	//  setLayer()				L.LayerGroup()         set layer search at runtime
+	//  showAlert()             'Text message'         show alert message
+	//  searchText()			'Text searched'        search text by external code
+	//
+	options: {
+		url: '',						//url for search by ajax request, ex: "search.php?q={s}". Can be function that returns string for dynamic parameter setting
+		layer: null,					//layer where search markers(is a L.LayerGroup)				
+		sourceData: null,				//function that fill _recordsCache, passed searching text by first param and callback in second				
+		//TODO implements uniq option 'sourceData' that recognizes source type: url,array,callback or layer				
+		jsonpParam: null,				//jsonp param name for search by jsonp service, ex: "callback"
+		propertyLoc: 'loc',				//field for remapping location, using array: ['latname','lonname'] for select double fields(ex. ['lat','lon'] ) support dotted format: 'prop.subprop.title'
+		propertyName: 'title',			//property in marker.options(or feature.properties for vector layer) trough filter elements in layer,
+		formatData: null,				//callback for reformat all data from source to indexed data object
+		filterData: null,				//callback for filtering data from text searched, params: textSearch, allRecords
+		moveToLocation: null,			//callback run on location found, params: latlng, title, map
+		buildTip: null,					//function that return row tip html node(or html string), receive text tooltip in first param
+		container: '',					//container id to insert Search Control		
+		zoom: null,						//default zoom level for move to location
+		minLength: 1,					//minimal text length for autocomplete
+		initial: true,					//search elements only by initial text
+		casesensitive: false,			//search elements in case sensitive text
+		autoType: true,					//complete input with first suggested result and select this filled-in text.
+		delayType: 400,					//delay while typing for show tooltip
+		tooltipLimit: -1,				//limit max results to show in tooltip. -1 for no limit, 0 for no results
+		tipAutoSubmit: true,			//auto map panTo when click on tooltip
+		firstTipSubmit: false,			//auto select first result con enter click
+		autoResize: true,				//autoresize on input change
+		collapsed: true,				//collapse search control at startup
+		autoCollapse: false,			//collapse search control after submit(on button or on tips if enabled tipAutoSubmit)
+		autoCollapseTime: 1200,			//delay for autoclosing alert and collapse after blur
+		textErr: 'Location not found',	//error message
+		textCancel: 'Cancel',		    //title in cancel button		
+		textPlaceholder: 'Search...',   //placeholder value			
+		position: 'topleft',
+		hideMarkerOnCollapse: false,    //remove circle and marker on search control collapsed		
+		marker: {						//custom L.Marker or false for hide
+			icon: false,				//custom L.Icon for maker location or false for hide
+			animate: true,				//animate a circle over location found
+			circle: {					//draw a circle in location found
+				radius: 10,
+				weight: 3,
+				color: '#e03',
+				stroke: true,
+				fill: false
+			}
+		}
+	},
+
+	initialize: function(options) {
+		L.Util.setOptions(this, options || {});
+		this._inputMinSize = this.options.textPlaceholder ? this.options.textPlaceholder.length : 10;
+		this._layer = this.options.layer || new L.LayerGroup();
+		this._filterData = this.options.filterData || this._defaultFilterData;
+		this._formatData = this.options.formatData || this._defaultFormatData;
+		this._moveToLocation = this.options.moveToLocation || this._defaultMoveToLocation;
+		this._autoTypeTmp = this.options.autoType;	//useful for disable autoType temporarily in delete/backspace keydown
+		this._countertips = 0;		//number of tips items
+		this._recordsCache = {};	//key,value table! that store locations! format: key,latlng
+		this._curReq = null;
+	},
+
+	onAdd: function (map) {
+		this._map = map;
+		this._container = L.DomUtil.create('div', 'leaflet-control-search');
+		this._input = this._createInput(this.options.textPlaceholder, 'search-input');
+		this._tooltip = this._createTooltip('search-tooltip');
+		this._cancel = this._createCancel(this.options.textCancel, 'search-cancel');
+		this._button = this._createButton(this.options.textPlaceholder, 'search-button');
+		this._alert = this._createAlert('search-alert');
+
+		if(this.options.collapsed===false)
+			this.expand(this.options.collapsed);
+
+		if(this.options.marker) {
+			
+			if(this.options.marker instanceof L.Marker || this.options.marker instanceof L.CircleMarker)
+				this._markerSearch = this.options.marker;
+
+			else if(_isObject(this.options.marker))
+				this._markerSearch = new L.Control.Search.Marker([0,0], this.options.marker);
+
+			this._markerSearch._isMarkerSearch = true;
+		}
+
+		this.setLayer( this._layer );
+
+		map.on({
+			// 		'layeradd': this._onLayerAddRemove,
+			// 		'layerremove': this._onLayerAddRemove
+			'resize': this._handleAutoresize
+			}, this);
+		return this._container;
+	},
+	addTo: function (map) {
+
+		if(this.options.container) {
+			this._container = this.onAdd(map);
+			this._wrapper = L.DomUtil.get(this.options.container);
+			this._wrapper.style.position = 'relative';
+			this._wrapper.appendChild(this._container);
+		}
+		else
+			L.Control.prototype.addTo.call(this, map);
+
+		return this;
+	},
+
+	onRemove: function(map) {
+		this._recordsCache = {};
+		// map.off({
+		// 		'layeradd': this._onLayerAddRemove,
+		// 		'layerremove': this._onLayerAddRemove
+		// 	}, this);
+	},
+
+	// _onLayerAddRemove: function(e) {
+	// 	//without this, run setLayer also for each Markers!! to optimize!
+	// 	if(e.layer instanceof L.LayerGroup)
+	// 		if( L.stamp(e.layer) != L.stamp(this._layer) )
+	// 			this.setLayer(e.layer);
+	// },
+
+	setLayer: function(layer) {	//set search layer at runtime
+		//this.options.layer = layer; //setting this, run only this._recordsFromLayer()
+		this._layer = layer;
+		this._layer.addTo(this._map);
+		return this;
+	},
+	
+	showAlert: function(text) {
+		text = text || this.options.textErr;
+		this._alert.style.display = 'block';
+		this._alert.innerHTML = text;
+		clearTimeout(this.timerAlert);
+		var that = this;		
+		this.timerAlert = setTimeout(function() {
+			that.hideAlert();
+		},this.options.autoCollapseTime);
+		return this;
+	},
+	
+	hideAlert: function() {
+		this._alert.style.display = 'none';
+		return this;
+	},
+		
+	cancel: function() {
+		this._input.value = '';
+		this._handleKeypress({ keyCode: 8 });//simulate backspace keypress
+		this._input.size = this._inputMinSize;
+		this._input.focus();
+		this._cancel.style.display = 'none';
+		this._hideTooltip();
+		return this;
+	},
+	
+	expand: function(toggle) {
+		toggle = typeof toggle === 'boolean' ? toggle : true;
+		this._input.style.display = 'block';
+		L.DomUtil.addClass(this._container, 'search-exp');
+		if ( toggle !== false ) {
+			this._input.focus();
+			this._map.on('dragstart click', this.collapse, this);
+		}
+		this.fire('search:expanded');
+		return this;	
+	},
+
+	collapse: function() {
+		this._hideTooltip();
+		this.cancel();
+		this._alert.style.display = 'none';
+		this._input.blur();
+		if(this.options.collapsed)
+		{
+			this._input.style.display = 'none';
+			this._cancel.style.display = 'none';			
+			L.DomUtil.removeClass(this._container, 'search-exp');		
+			if (this.options.hideMarkerOnCollapse) {
+				this._map.removeLayer(this._markerSearch);
+			}
+			this._map.off('dragstart click', this.collapse, this);
+		}
+		this.fire('search:collapsed');
+		return this;
+	},
+	
+	collapseDelayed: function() {	//collapse after delay, used on_input blur
+		if (!this.options.autoCollapse) return this;
+		var that = this;
+		clearTimeout(this.timerCollapse);
+		this.timerCollapse = setTimeout(function() {
+			that.collapse();
+		}, this.options.autoCollapseTime);
+		return this;		
+	},
+
+	collapseDelayedStop: function() {
+		clearTimeout(this.timerCollapse);
+		return this;		
+	},
+
+	////start DOM creations
+	_createAlert: function(className) {
+		var alert = L.DomUtil.create('div', className, this._container);
+		alert.style.display = 'none';
+
+		L.DomEvent
+			.on(alert, 'click', L.DomEvent.stop, this)
+			.on(alert, 'click', this.hideAlert, this);
+
+		return alert;
+	},
+
+	_createInput: function (text, className) {
+		var label = L.DomUtil.create('label', className, this._container);
+		var input = L.DomUtil.create('input', className, this._container);
+		input.type = 'text';
+		input.size = this._inputMinSize;
+		input.value = '';
+		input.autocomplete = 'off';
+		input.autocorrect = 'off';
+		input.autocapitalize = 'off';
+		input.placeholder = text;
+		input.style.display = 'none';
+		input.role = 'search';
+		input.id = input.role + input.type + input.size;
+		
+		label.htmlFor = input.id;
+		label.style.display = 'none';
+		label.value = text;
+
+		L.DomEvent
+			.disableClickPropagation(input)
+			.on(input, 'keydown', this._handleKeypress, this)
+			.on(input, 'blur', this.collapseDelayed, this)
+			.on(input, 'focus', this.collapseDelayedStop, this);
+		
+		return input;
+	},
+
+	_createCancel: function (title, className) {
+		var cancel = L.DomUtil.create('a', className, this._container);
+		cancel.href = '#';
+		cancel.title = title;
+		cancel.style.display = 'none';
+		cancel.innerHTML = "<span>&otimes;</span>";//imageless(see css)
+
+		L.DomEvent
+			.on(cancel, 'click', L.DomEvent.stop, this)
+			.on(cancel, 'click', this.cancel, this);
+
+		return cancel;
+	},
+	
+	_createButton: function (title, className) {
+		var button = L.DomUtil.create('a', className, this._container);
+		button.href = '#';
+		button.title = title;
+
+		L.DomEvent
+			.on(button, 'click', L.DomEvent.stop, this)
+			.on(button, 'click', this._handleSubmit, this)			
+			.on(button, 'focus', this.collapseDelayedStop, this)
+			.on(button, 'blur', this.collapseDelayed, this);
+
+		return button;
+	},
+
+	_createTooltip: function(className) {
+		var tool = L.DomUtil.create('ul', className, this._container);
+		tool.style.display = 'none';
+
+		var that = this;
+		L.DomEvent
+			.disableClickPropagation(tool)
+			.on(tool, 'blur', this.collapseDelayed, this)
+			.on(tool, 'mousewheel', function(e) {
+				that.collapseDelayedStop();
+				L.DomEvent.stopPropagation(e);//disable zoom map
+			}, this)
+			.on(tool, 'mouseover', function(e) {
+				that.collapseDelayedStop();
+			}, this);
+		return tool;
+	},
+
+	_createTip: function(text, val) {//val is object in recordCache, usually is Latlng
+		var tip;
+		
+		if(this.options.buildTip)
+		{
+			tip = this.options.buildTip.call(this, text, val); //custom tip node or html string
+			if(typeof tip === 'string')
+			{
+				var tmpNode = L.DomUtil.create('div');
+				tmpNode.innerHTML = tip;
+				tip = tmpNode.firstChild;
+			}
+		}
+		else
+		{
+			tip = L.DomUtil.create('li', '');
+			tip.innerHTML = text;
+		}
+		
+		L.DomUtil.addClass(tip, 'search-tip');
+		tip._text = text; //value replaced in this._input and used by _autoType
+
+		if(this.options.tipAutoSubmit)
+			L.DomEvent
+				.disableClickPropagation(tip)		
+				.on(tip, 'click', L.DomEvent.stop, this)
+				.on(tip, 'click', function(e) {
+					this._input.value = text;
+					this._handleAutoresize();
+					this._input.focus();
+					this._hideTooltip();	
+					this._handleSubmit();
+				}, this);
+
+		return tip;
+	},
+
+	//////end DOM creations
+
+	_getUrl: function(text) {
+		return (typeof this.options.url === 'function') ? this.options.url(text) : this.options.url;
+	},
+
+	_defaultFilterData: function(text, records) {
+	
+		var I, icase, regSearch, frecords = {};
+
+		text = text.replace(/[.*+?^${}()|[\]\\]/g, '');  //sanitize remove all special characters
+		if(text==='')
+			return [];
+
+		I = this.options.initial ? '^' : '';  //search only initial text
+		icase = !this.options.casesensitive ? 'i' : undefined;
+
+		regSearch = new RegExp(I + text, icase);
+
+		//TODO use .filter or .map
+		for(var key in records) {
+			if( regSearch.test(key) )
+				frecords[key]= records[key];
+		}
+		
+		return frecords;
+	},
+
+	showTooltip: function(records) {
+		
+
+		this._countertips = 0;
+		this._tooltip.innerHTML = '';
+		this._tooltip.currentSelection = -1;  //inizialized for _handleArrowSelect()
+
+		if(this.options.tooltipLimit)
+		{
+			for(var key in records)//fill tooltip
+			{
+				if(this._countertips === this.options.tooltipLimit)
+					break;
+				
+				this._countertips++;
+
+				this._tooltip.appendChild( this._createTip(key, records[key]) );
+			}
+		}
+		
+		if(this._countertips > 0)
+		{
+			this._tooltip.style.display = 'block';
+			
+			if(this._autoTypeTmp)
+				this._autoType();
+
+			this._autoTypeTmp = this.options.autoType;//reset default value
+		}
+		else
+			this._hideTooltip();
+
+		this._tooltip.scrollTop = 0;
+
+		return this._countertips;
+	},
+
+	_hideTooltip: function() {
+		this._tooltip.style.display = 'none';
+		this._tooltip.innerHTML = '';
+		return 0;
+	},
+
+	_defaultFormatData: function(json) {	//default callback for format data to indexed data
+		var propName = this.options.propertyName,
+			propLoc = this.options.propertyLoc,
+			i, jsonret = {};
+
+		if( L.Util.isArray(propLoc) )
+			for(i in json)
+				jsonret[ _getPath(json[i],propName) ]= L.latLng( json[i][ propLoc[0] ], json[i][ propLoc[1] ] );
+		else
+			for(i in json)
+				jsonret[ _getPath(json[i],propName) ]= L.latLng( _getPath(json[i],propLoc) );
+		//TODO throw new Error("propertyName '"+propName+"' not found in JSON data");
+		return jsonret;
+	},
+
+	_recordsFromJsonp: function(text, callAfter) {  //extract searched records from remote jsonp service
+		L.Control.Search.callJsonp = callAfter;
+		var script = L.DomUtil.create('script','leaflet-search-jsonp', document.getElementsByTagName('body')[0] ),			
+			url = L.Util.template(this._getUrl(text)+'&'+this.options.jsonpParam+'=L.Control.Search.callJsonp', {s: text}); //parsing url
+			//rnd = '&_='+Math.floor(Math.random()*10000);
+			//TODO add rnd param or randomize callback name! in recordsFromJsonp
+		script.type = 'text/javascript';
+		script.src = url;
+		return { abort: function() { script.parentNode.removeChild(script); } };
+	},
+
+	_recordsFromAjax: function(text, callAfter) {	//Ajax request
+		if (window.XMLHttpRequest === undefined) {
+			window.XMLHttpRequest = function() {
+				try { return new ActiveXObject("Microsoft.XMLHTTP.6.0"); }
+				catch  (e1) {
+					try { return new ActiveXObject("Microsoft.XMLHTTP.3.0"); }
+					catch (e2) { throw new Error("XMLHttpRequest is not supported"); }
+				}
+			};
+		}
+		var IE8or9 = ( L.Browser.ie && !window.atob && document.querySelector ),
+			request = IE8or9 ? new XDomainRequest() : new XMLHttpRequest(),
+			url = L.Util.template(this._getUrl(text), {s: text});
+
+		//rnd = '&_='+Math.floor(Math.random()*10000);
+		//TODO add rnd param or randomize callback name! in recordsFromAjax			
+		
+		request.open("GET", url);
+		var that = this;
+
+		request.onload = function() {
+			callAfter( JSON.parse(request.responseText) );
+		};
+		request.onreadystatechange = function() {
+		    if(request.readyState === 4 && request.status === 200) {
+		    	this.onload();
+		    }
+		};
+
+		request.send();
+		return request;   
+	},
+	
+	_recordsFromLayer: function() {	//return table: key,value from layer
+		var that = this,
+			retRecords = {},
+			propName = this.options.propertyName,
+			loc;
+		
+		this._layer.eachLayer(function(layer) {
+
+			if(layer.hasOwnProperty('_isMarkerSearch')) return;
+
+			if(layer instanceof L.Marker || layer instanceof L.CircleMarker)
+			{
+				try {
+					if(_getPath(layer.options,propName))
+					{
+						loc = layer.getLatLng();
+						loc.layer = layer;
+						retRecords[ _getPath(layer.options,propName) ] = loc;			
+						
+					}
+					else if(_getPath(layer.feature.properties,propName)){
+	
+						loc = layer.getLatLng();
+						loc.layer = layer;
+						retRecords[ _getPath(layer.feature.properties,propName) ] = loc;
+						
+					}
+					else
+						throw new Error("propertyName '"+propName+"' not found in marker");
+					
+				}
+				catch(err){
+					if (console) {  }
+				}
+			}
+            else if(layer.hasOwnProperty('feature'))//GeoJSON
+			{
+				try {
+					if(layer.feature.properties.hasOwnProperty(propName))
+					{
+						loc = layer.getBounds().getCenter();
+						loc.layer = layer;			
+						retRecords[ layer.feature.properties[propName] ] = loc;
+					}
+					else
+						throw new Error("propertyName '"+propName+"' not found in feature");
+				}
+				catch(err){
+					if (console) {  }
+				}
+			}
+			else if(layer instanceof L.LayerGroup)
+            {
+                //TODO: Optimize
+                layer.eachLayer(function(m) {
+                    loc = m.getLatLng();
+                    loc.layer = m;
+                    retRecords[ m.feature.properties[propName] ] = loc;
+                });
+            }
+			
+		},this);
+		
+		return retRecords;
+	},
+
+	_autoType: function() {
+		
+		//TODO implements autype without selection(useful for mobile device)
+		
+		var start = this._input.value.length,
+			firstRecord = this._tooltip.firstChild ? this._tooltip.firstChild._text : '',
+			end = firstRecord.length;
+
+		if (firstRecord.indexOf(this._input.value) === 0) { // If prefix match
+			this._input.value = firstRecord;
+			this._handleAutoresize();
+
+			if (this._input.createTextRange) {
+				var selRange = this._input.createTextRange();
+				selRange.collapse(true);
+				selRange.moveStart('character', start);
+				selRange.moveEnd('character', end);
+				selRange.select();
+			}
+			else if(this._input.setSelectionRange) {
+				this._input.setSelectionRange(start, end);
+			}
+			else if(this._input.selectionStart) {
+				this._input.selectionStart = start;
+				this._input.selectionEnd = end;
+			}
+		}
+	},
+
+	_hideAutoType: function() {	// deselect text:
+
+		var sel;
+		if ((sel = this._input.selection) && sel.empty) {
+			sel.empty();
+		}
+		else if (this._input.createTextRange) {
+			sel = this._input.createTextRange();
+			sel.collapse(true);
+			var end = this._input.value.length;
+			sel.moveStart('character', end);
+			sel.moveEnd('character', end);
+			sel.select();
+		}
+		else {
+			if (this._input.getSelection) {
+				this._input.getSelection().removeAllRanges();
+			}
+			this._input.selectionStart = this._input.selectionEnd;
+		}
+	},
+	
+	_handleKeypress: function (e) {	//run _input keyup event
+
+		switch(e.keyCode)
+		{
+			case 27://Esc
+				this.collapse();
+			break;
+			case 13://Enter
+				if(this._countertips == 1 || (this.options.firstTipSubmit && this._countertips > 0))
+					this._handleArrowSelect(1);
+				this._handleSubmit();	//do search
+			break;
+			case 38://Up
+				this._handleArrowSelect(-1);
+			break;
+			case 40://Down
+				this._handleArrowSelect(1);
+			break;
+			case  8://Backspace
+			case 45://Insert
+			case 46://Delete
+				this._autoTypeTmp = false;//disable temporarily autoType
+			break;
+			case 37://Left
+			case 39://Right
+			case 16://Shift
+			case 17://Ctrl
+			case 35://End
+			case 36://Home
+			break;
+			default://All keys
+
+				if(this._input.value.length)
+					this._cancel.style.display = 'block';
+				else
+					this._cancel.style.display = 'none';
+
+				if(this._input.value.length >= this.options.minLength)
+				{
+					var that = this;
+
+					clearTimeout(this.timerKeypress);	//cancel last search request while type in				
+					this.timerKeypress = setTimeout(function() {	//delay before request, for limit jsonp/ajax request
+
+						that._fillRecordsCache();
+					
+					}, this.options.delayType);
+				}
+				else
+					this._hideTooltip();
+		}
+
+		this._handleAutoresize();
+	},
+
+	searchText: function(text) {
+		var code = text.charCodeAt(text.length);
+
+		this._input.value = text;
+
+		this._input.style.display = 'block';
+		L.DomUtil.addClass(this._container, 'search-exp');
+
+		this._autoTypeTmp = false;
+
+		this._handleKeypress({keyCode: code});
+	},
+	
+	_fillRecordsCache: function() {
+
+		var inputText = this._input.value,
+			that = this, records;
+
+		if(this._curReq && this._curReq.abort)
+			this._curReq.abort();
+		//abort previous requests
+
+		L.DomUtil.addClass(this._container, 'search-load');	
+
+		if(this.options.layer)
+		{
+			//TODO _recordsFromLayer must return array of objects, formatted from _formatData
+			this._recordsCache = this._recordsFromLayer();
+			
+			records = this._filterData( this._input.value, this._recordsCache );
+
+			this.showTooltip( records );
+
+			L.DomUtil.removeClass(this._container, 'search-load');
+		}
+		else
+		{
+			if(this.options.sourceData)
+				this._retrieveData = this.options.sourceData;
+
+			else if(this.options.url)	//jsonp or ajax
+				this._retrieveData = this.options.jsonpParam ? this._recordsFromJsonp : this._recordsFromAjax;
+
+			this._curReq = this._retrieveData.call(this, inputText, function(data) {
+				
+				that._recordsCache = that._formatData(data);
+
+				//TODO refact!
+				if(that.options.sourceData)
+					records = that._filterData( that._input.value, that._recordsCache );
+				else
+					records = that._recordsCache;
+
+				that.showTooltip( records );
+ 
+				L.DomUtil.removeClass(that._container, 'search-load');
+			});
+		}
+	},
+	
+	_handleAutoresize: function() {	//autoresize this._input
+	    //TODO refact _handleAutoresize now is not accurate
+	    if (this._input.style.maxWidth != this._map._container.offsetWidth) //If maxWidth isn't the same as when first set, reset to current Map width
+	        this._input.style.maxWidth = L.DomUtil.getStyle(this._map._container, 'width');
+
+		if(this.options.autoResize && (this._container.offsetWidth + 45 < this._map._container.offsetWidth))
+			this._input.size = this._input.value.length<this._inputMinSize ? this._inputMinSize : this._input.value.length;
+	},
+
+	_handleArrowSelect: function(velocity) {
+	
+		var searchTips = this._tooltip.hasChildNodes() ? this._tooltip.childNodes : [];
+			
+		for (i=0; i<searchTips.length; i++)
+			L.DomUtil.removeClass(searchTips[i], 'search-tip-select');
+		
+		if ((velocity == 1 ) && (this._tooltip.currentSelection >= (searchTips.length - 1))) {// If at end of list.
+			L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select');
+		}
+		else if ((velocity == -1 ) && (this._tooltip.currentSelection <= 0)) { // Going back up to the search box.
+			this._tooltip.currentSelection = -1;
+		}
+		else if (this._tooltip.style.display != 'none') {
+			this._tooltip.currentSelection += velocity;
+			
+			L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select');
+			
+			this._input.value = searchTips[this._tooltip.currentSelection]._text;
+
+			// scroll:
+			var tipOffsetTop = searchTips[this._tooltip.currentSelection].offsetTop;
+			
+			if (tipOffsetTop + searchTips[this._tooltip.currentSelection].clientHeight >= this._tooltip.scrollTop + this._tooltip.clientHeight) {
+				this._tooltip.scrollTop = tipOffsetTop - this._tooltip.clientHeight + searchTips[this._tooltip.currentSelection].clientHeight;
+			}
+			else if (tipOffsetTop <= this._tooltip.scrollTop) {
+				this._tooltip.scrollTop = tipOffsetTop;
+			}
+		}
+	},
+
+	_handleSubmit: function() {	//button and tooltip click and enter submit
+
+		this._hideAutoType();
+		
+		this.hideAlert();
+		this._hideTooltip();
+
+		if(this._input.style.display == 'none')	//on first click show _input only
+			this.expand();
+		else
+		{
+			if(this._input.value === '')	//hide _input only
+				this.collapse();
+			else
+			{
+				var loc = this._getLocation(this._input.value);
+				
+				if(loc===false)
+					this.showAlert();
+				else
+				{
+					this.showLocation(loc, this._input.value);
+					this.fire('search:locationfound', {
+							latlng: loc,
+							text: this._input.value,
+							layer: loc.layer ? loc.layer : null
+						});
+				}
+			}
+		}
+	},
+
+	_getLocation: function(key) {	//extract latlng from _recordsCache
+
+		if( this._recordsCache.hasOwnProperty(key) )
+			return this._recordsCache[key];//then after use .loc attribute
+		else
+			return false;
+	},
+
+	_defaultMoveToLocation: function(latlng, title, map) {
+		if(this.options.zoom)
+ 			this._map.setView(latlng, this.options.zoom);
+ 		else
+			this._map.panTo(latlng);
+	},
+
+	showLocation: function(latlng, title) {	//set location on map from _recordsCache
+		var self = this;
+
+		self._map.once('moveend zoomend', function(e) {
+
+			if(self._markerSearch) {
+				self._markerSearch.addTo(self._map).setLatLng(latlng);
+			}
+			
+		});
+
+		self._moveToLocation(latlng, title, self._map);
+		//FIXME autoCollapse option hide self._markerSearch before that visualized!!
+		if(self.options.autoCollapse)
+			self.collapse();
+
+		return self;
+	}
+});
+
+L.Control.Search.Marker = L.Marker.extend({
+
+	includes: L.Mixin.Events,
+	
+	options: {
+		icon: new L.Icon.Default(),
+		animate: true,
+		circle: {
+			radius: 10,
+			weight: 3,
+			color: '#e03',
+			stroke: true,
+			fill: false
+		}
+	},
+	
+	initialize: function (latlng, options) {
+		L.setOptions(this, options);
+
+		if(options.icon === true)
+			options.icon = new L.Icon.Default();
+
+		L.Marker.prototype.initialize.call(this, latlng, options);
+		
+		if( _isObject(this.options.circle) )
+			this._circleLoc = new L.CircleMarker(latlng, this.options.circle);
+	},
+
+	onAdd: function (map) {
+		L.Marker.prototype.onAdd.call(this, map);
+		if(this._circleLoc) {
+			map.addLayer(this._circleLoc);
+			if(this.options.animate)
+				this.animate();
+		}
+	},
+
+	onRemove: function (map) {
+		L.Marker.prototype.onRemove.call(this, map);
+		if(this._circleLoc)
+			map.removeLayer(this._circleLoc);
+	},
+	
+	setLatLng: function (latlng) {
+		L.Marker.prototype.setLatLng.call(this, latlng);
+		if(this._circleLoc)
+			this._circleLoc.setLatLng(latlng);
+		return this;
+	},
+	
+	_initIcon: function () {
+		if(this.options.icon)
+			L.Marker.prototype._initIcon.call(this);
+	},
+
+	_removeIcon: function () {
+		if(this.options.icon)
+			L.Marker.prototype._removeIcon.call(this);
+	},
+
+	animate: function() {
+	//TODO refact animate() more smooth! like this: http://goo.gl/DDlRs
+		if(this._circleLoc)
+		{
+			var circle = this._circleLoc,
+				tInt = 200,	//time interval
+				ss = 5,	//frames
+				mr = parseInt(circle._radius/ss),
+				oldrad = this.options.circle.radius,
+				newrad = circle._radius * 2,
+				acc = 0;
+
+			circle._timerAnimLoc = setInterval(function() {
+				acc += 0.5;
+				mr += acc;	//adding acceleration
+				newrad -= mr;
+				
+				circle.setRadius(newrad);
+
+				if(newrad<oldrad)
+				{
+					clearInterval(circle._timerAnimLoc);
+					circle.setRadius(oldrad);//reset radius
+					//if(typeof afterAnimCall == 'function')
+						//afterAnimCall();
+						//TODO use create event 'animateEnd' in L.Control.Search.Marker 
+				}
+			}, tInt);
+		}
+		
+		return this;
+	}
+});
+
+L.Map.addInitHook(function () {
+    if (this.options.searchControl) {
+        this.searchControl = L.control.search(this.options.searchControl);
+        this.addControl(this.searchControl);
+    }
+});
+
+L.control.search = function (options) {
+    return new L.Control.Search(options);
+};
+
+return L.Control.Search;
+
+});
+
+

+ 5 - 0
Resources/views/CRUD/map_show_field.html.twig

@@ -0,0 +1,5 @@
+{% extends "SonataAdminBundle:CRUD:base_show_field.html.twig" %}
+
+{% block field %}
+    {{ include('LeafletBundle:Leaflet:map.html.twig') }}
+{% endblock %}

+ 9 - 0
Resources/views/Form/Type/leaflet_map_widget.html.twig

@@ -0,0 +1,9 @@
+{% block leaflet_map_widget %}
+    {% spaceless %}
+        <div class="form-group">
+            {% include 'LeafletBundle:Leaflet:map.html.twig' with { 'extra_js': true } %}
+            
+            {{ block('hidden_widget') }}
+        </div>
+    {% endspaceless %}
+{% endblock %}

+ 22 - 0
Resources/views/Leaflet/map.html.twig

@@ -0,0 +1,22 @@
+<div id="map" class="leaflet-map-widget"></div>
+
+<style type="text/css">
+    .leaflet-map-widget {
+        height: 400px; 
+        border: 1px solid #CCCCCC;
+    }
+</style>
+{% include 'LeafletBundle:Leaflet:resources.html.twig' %}
+
+{% if object is defined %}
+<script type="text/javascript">
+    var dataValue = '{{ object.data|raw }}';
+</script>
+{% endif %}
+
+<script src="{{ asset('bundles/leaflet/js/leaflet-map.js') }}"></script>
+
+{% if extra_js is defined %}
+<script src="{{ script_google_maps }}"></script>
+<script src="{{ asset('bundles/leaflet/js/leaflet-map-widget.js') }}"></script>
+{% endif %}

+ 3 - 0
Resources/views/Leaflet/resources.html.twig

@@ -1,7 +1,10 @@
 <link rel="stylesheet" href="{{ asset('bundles/leaflet/leaflet/leaflet.css') }}" />
 <link rel="stylesheet" href="{{ asset('bundles/leaflet/leaflet.draw.css') }}" />
+<link rel="stylesheet" href="{{ asset('bundles/leaflet/leaflet/leaflet-search.css') }}" />
 
 <script type="text/javascript" src="{{ asset('bundles/leaflet/leaflet/leaflet-src.js') }}" ></script>
+<script type="text/javascript" src="{{ asset('bundles/leaflet/leaflet/leaflet-search.src.js') }}" ></script>
+<script type="text/javascript" src="{{ asset('bundles/leaflet/leaflet/leaflet-search-geocoder.js') }}" ></script>
 
 <script type="text/javascript" src="{{ asset('bundles/leaflet/Leaflet.draw.js') }}" ></script>
 <script type="text/javascript" src="{{ asset('bundles/leaflet/Leaflet.Draw.Event.js') }}" ></script>