From fb2bde41ac04ee93c4dd238dcb3e812ac2d15356 Mon Sep 17 00:00:00 2001 From: Christian Franke Date: Thu, 12 Dec 2013 16:10:32 +0100 Subject: Add search functionality --- web/css/leaflet.css | 2 +- web/css/leaflet.search.css | 134 +++++++ web/index.html | 2 + web/js/eventmap.js | 52 ++- web/js/leaflet.search.js | 847 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1035 insertions(+), 2 deletions(-) create mode 100644 web/css/leaflet.search.css create mode 100644 web/js/leaflet.search.js (limited to 'web') diff --git a/web/css/leaflet.css b/web/css/leaflet.css index 0b08270..adfb9f6 100644 --- a/web/css/leaflet.css +++ b/web/css/leaflet.css @@ -181,7 +181,7 @@ /* visual tweaks */ .leaflet-container { - background: #ddd; + background: #fff; outline: 0; } .leaflet-container a { diff --git a/web/css/leaflet.search.css b/web/css/leaflet.search.css new file mode 100644 index 0000000..186e4cc --- /dev/null +++ b/web/css/leaflet.search.css @@ -0,0 +1,134 @@ +/* + * Leaflet Search Control 1.4.6 + * Copyright 2013, Stefano Cudini - http://labs.easyblog.it/stefano-cudini/ + * Licensed under the MIT license. + * + * Demo: + * http://labs.easyblog.it/maps/leaflet-search + * + * Repositories: + * https://github.com/stefanocudini/leaflet-search + * https://bitbucket.org/zakis_/leaflet-search + * + */ + +.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; + border-radius:4px; +} +.leaflet-control-search .search-button:hover { + background: url('images/search-icon.png') no-repeat 2px -22px; +} +.leaflet-control-search .search-tooltip { + position:absolute; + top:100%; + left:0; + float:left; + min-width:80px; + max-height:106px;/*(.search-tip height * 5)*/ + box-shadow: 0 0 8px rgba(0,0,0,0.4); + -webkit-border-radius: 5px; + -webkit-border-top-left-radius: 0; + -moz-border-radius: 5px; + -moz-border-radius-topleft: 0; + border-radius: 5px; + border-top-left-radius: 0; + background-color: rgba(0, 0, 0, 0.25); + z-index:1010; + overflow-y:auto; + overflow-x:hidden; +} +.leaflet-control-search .search-tip { + font-size:.85em; + margin:2px; + padding:2px; + display:block; + color:black; + background: #ddd; + border-radius:.25em; + text-decoration:none; + white-space:nowrap; + font-size:.85em; + vertical-align:center; +} +.leaflet-control-search .search-tip-select, +.leaflet-control-search .search-tip:hover, +.leaflet-control-search .search-button: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; +} + + diff --git a/web/index.html b/web/index.html index 388715b..fbe52c0 100644 --- a/web/index.html +++ b/web/index.html @@ -6,6 +6,7 @@ + @@ -20,6 +21,7 @@ + diff --git a/web/js/eventmap.js b/web/js/eventmap.js index 4ad23fa..376cba5 100644 --- a/web/js/eventmap.js +++ b/web/js/eventmap.js @@ -238,15 +238,60 @@ $(function() { map = L.map('map', { center: new L.LatLng(70,-50), contextmenu: true, + continuousWorld: true, zoom: 2 }); map.on('zoomend', marker_labels_calc_nohide); draw_control = new L.Control.Draw({ + draw: { + polyline: false, + polygon: false, + rectangle: false, + circle: false, + } }); map.addControl(draw_control); + var search_control = new L.Control.Search({ + callData: function(input, callback) { + var input_lower = input.toLowerCase(); + var records = []; + + for (var key in marker_store) { + if (key.toLowerCase().indexOf(input_lower) != 0) + continue; + + var marker = marker_store[key]; + records.push({ + title: key, + loc: marker.getLatLng() + }); + } + records.sort(function(a,b) { + if (a.title < b.title) + return -1; + if (a.title > b.title) + return 1; + return 0; + }); + callback(records); + } + }); + search_control.on('search_locationfound', function(location_info) { + var marker = marker_store[location_info.text]; + for (var key in layers) { + map.removeLayer(layers[key]); + } + map.addLayer(layers[marker.options.layer_name]); + if (search_control._markerLoc !== undefined) { + search_control._markerLoc.show(); + search_control._markerLoc.animate(); + } + }); + map.addControl(search_control); + map.on('draw:created', function (e) { var created_object_type = e.layerType; var created_object = e.layer; @@ -300,7 +345,12 @@ $(function() { first_layer = false; } }); - L.control.layers(layers, {}).addTo(map); + var layer_control = L.control.layers(layers, {}); + map.on('baselayerchange', function() { + if (search_control._markerLoc !== undefined) + search_control._markerLoc.hide(); + }); + layer_control.addTo(map); $.ajax({ url: 'api/markers/get' diff --git a/web/js/leaflet.search.js b/web/js/leaflet.search.js new file mode 100644 index 0000000..fdc3269 --- /dev/null +++ b/web/js/leaflet.search.js @@ -0,0 +1,847 @@ +/* + * Leaflet Search Control 1.4.6 + * Copyright 2013, Stefano Cudini - http://labs.easyblog.it/stefano-cudini/ + * Licensed under the MIT license. + * + * Demo: + * http://labs.easyblog.it/maps/leaflet-search + * + * Repositories: + * https://github.com/stefanocudini/leaflet-search + * https://bitbucket.org/zakis_/leaflet-search + * + */ +(function() { + +L.Control.Search = L.Control.extend({ + includes: L.Mixin.Events, + // + // Name Data passed Description + // + //Managed Events: + // search_locationfound {latlng, title} fired after moved and show markerLocation + // search_collapsed {} fired after control was collapsed + // + //Public methods: + // setLayer() L.LayerGroup() set layer search at runtime + // showAlert() 'Text message' Show alert message + // + options: { + url: '', //url for search by ajax request, ex: "search.php?q={s}" + jsonpParam: null, //jsonp param name for search by jsonp service, ex: "callback" + layer: null, //layer where search markers(is a L.LayerGroup) + callData: null, //function that fill _recordsCache, passed searching text by first param and callback in second + //TODO important! implements uniq option 'sourceData' that recognizes source type: url,array,callback or layer + //TODO implement can do research on multiple sources + propertyName: 'title', //property in marker.options(or feature.properties for vector layer) trough filter elements in layer + propertyLoc: 'loc', //field name for remapping location, using array: ['latname','lonname'] for select double fields(ex. ['lat','lon'] ) + //TODO implement sub property filter for propertyName,propertyLoc like this: "prop.subprop.title" + callTip: null, //function that return row tip html node(or html string), receive text tooltip in first param + filterJSON: null, //callback for filtering data to _recordsCache + minLength: 1, //minimal text length for autocomplete + initial: true, //search elements only by initial 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. + tipAutoSubmit: true, //auto map panTo when click on tooltip + autoResize: true, //autoresize on input change + autoCollapse: false, //collapse search control after submit(on button or on tips if enabled tipAutoSubmit) + //TODO add option for persist markerLoc after collapse! + autoCollapseTime: 1200, //delay for autoclosing alert and collapse after blur + animateLocation: true, //animate a circle over location found + circleLocation: true, //draw a circle in location found + markerLocation: false, //draw a marker in location found + zoom: null, //zoom after pan to location found, default: map.getZoom() + text: 'Search...', //placeholder value + textCancel: 'Cancel', //title in cancel button + textErr: 'Location not found', //error message + position: 'topleft' + //TODO add option collapsed, like control.layers + }, +//FIXME option condition problem {autoCollapse: true, markerLocation: true} not show location +//FIXME option condition problem {autoCollapse: false } + + initialize: function(options) { + L.Util.setOptions(this, options || {}); + this._inputMinSize = this.options.text ? this.options.text.length : 10; + this._layer = this.options.layer || new L.LayerGroup(); + this._filterJSON = this.options.filterJSON || this._defaultFilterJSON; + 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 + }, + + onAdd: function (map) { + this._map = map; + this._container = L.DomUtil.create('div', 'leaflet-control-search'); + this._input = this._createInput(this.options.text, 'search-input'); + this._tooltip = this._createTooltip('search-tooltip'); + this._cancel = this._createCancel(this.options.textCancel, 'search-cancel'); + this._button = this._createButton(this.options.text, 'search-button'); + this._alert = this._createAlert('search-alert'); + + if(this.options.circleLocation || this.options.markerLocation) + this._markerLoc = new SearchMarker([0,0], {marker: this.options.markerLocation});//see below + + this.setLayer( this._layer ); + map.on({ + // 'layeradd': this._onLayerAddRemove, + // 'layerremove': this._onLayerAddRemove + 'resize':this._handleAutoresize() + }, this); + return this._container; + }, + + onRemove: function(map) { + this._recordsCache = {}; + // map.off({ + // 'layeradd': this._onLayerAddRemove, + // 'layerremove': this._onLayerAddRemove + // }, this); + }, + + // _onLayerAddRemove: function(e) { + // //console.info('_onLayerAddRemove'); + // //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); + if(this._markerLoc) + this._layer.addLayer(this._markerLoc); + 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'; + return this; + }, + + expand: function() { + this._input.style.display = 'block'; + L.DomUtil.addClass(this._container, 'search-exp'); + this._input.focus(); + this._map.on('dragstart', this.collapse, this); + return this; + }, + + collapse: function() { + this._hideTooltip(); + this.cancel(); + this._alert.style.display = 'none'; + this._input.style.display = 'none'; + this._input.blur(); + this._cancel.style.display = 'none'; + L.DomUtil.removeClass(this._container, 'search-exp'); + //this._markerLoc.hide();//maybe unuseful + this._map.off('dragstart', 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 input = L.DomUtil.create('input', className, this._container); + input.type = 'text'; + input.size = this._inputMinSize; + input.value = ''; + input.autocomplete = 'off'; + input.placeholder = text; + input.style.display = 'none'; + + L.DomEvent + .disableClickPropagation(input) + .on(input, 'keyup', this._handleKeypress, this) + .on(input, 'keydown', this._handleAutoresize, 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 = "";//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('div', 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.callTip) + { + tip = this.options.callTip(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('a', ''); + tip.href = '#'; + tip.innerHTML = text; + } + + L.DomUtil.addClass(tip, 'search-tip'); + tip._text = text; //value replaced in this._input and used by _autoType + + 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(); + if(this.options.tipAutoSubmit)//go to location at once + this._handleSubmit(); + }, this); + + return tip; + }, + +//////end DOM creations + + _filterRecords: function(text) { //Filter this._recordsCache case insensitive and much more.. + + var regFilter = new RegExp("^[.]$|[\[\]|()*]",'g'), //remove . * | ( ) ] [ + text = text.replace(regFilter,''), //sanitize text + I = this.options.initial ? '^' : '', //search only initial text + regSearch = new RegExp(I + text,'i'), + //TODO add option for case sesitive search, also showLocation + frecords = {}; + + //TODO use .filter or .map + for(var key in this._recordsCache) + if( regSearch.test(key) ) + frecords[key]= this._recordsCache[key]; + + return frecords; + }, + + showTooltip: function() { + var filteredRecords, newTip; + + this._countertips = 0; + + //FIXME problem with jsonp/ajax when remote filter has different behavior of this._filterRecords + if(this.options.layer) + filteredRecords = this._filterRecords( this._input.value ); + else + filteredRecords = this._recordsCache; + + this._tooltip.innerHTML = ''; + this._tooltip.currentSelection = -1; //inizialized for _handleArrowSelect() + + for(var key in filteredRecords)//fill tooltip + { + if(++this._countertips == this.options.tooltipLimit) break; + + newTip = this._createTip(key, filteredRecords[key] ); + + this._tooltip.appendChild(newTip); + } + + 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; + }, + + _defaultFilterJSON: function(json) { //default callback for filter data + var jsonret = {}, + propName = this.options.propertyName, + propLoc = this.options.propertyLoc; + + if( L.Util.isArray(propLoc) ) + for(var i in json) + jsonret[ json[i][propName] ]= L.latLng( json[i][ propLoc[0] ], json[i][ propLoc[1] ] ); + else + for(var i in json) + jsonret[ json[i][propName] ]= L.latLng( json[i][ propLoc ] ); + //TODO verify json[i].hasOwnProperty(propName) + //throw new Error("propertyName '"+propName+"' not found in JSON data"); + return jsonret; + }, + + _recordsFromJsonp: function(text, callAfter) { //extract searched records from remote jsonp service + //TODO remove script node after call run + var that = this; + L.Control.Search.callJsonp = function(data) { //jsonp callback + var fdata = that._filterJSON(data);//_filterJSON defined in inizialize... + callAfter(fdata); + } + var script = L.DomUtil.create('script','search-jsonp', document.getElementsByTagName('body')[0] ), + url = L.Util.template(this.options.url+'&'+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 this; + //may be 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 request = new XMLHttpRequest(), + url = L.Util.template(this.options.url, {s: text}), //parsing url + //rnd = '&_='+Math.floor(Math.random()*10000); + //TODO add rnd param or randomize callback name! in recordsFromAjax + response = {}; + + request.open("GET", url); + var that = this; + request.onreadystatechange = function() { + if(request.readyState === 4 && request.status === 200) { + response = window.JSON ? JSON.parse(request.responseText) : eval("("+ request.responseText + ")"); + var fdata = that._filterJSON(response);//_filterJSON defined in inizialize... + callAfter(fdata); + } + }; + request.send(); + return this; + }, + + _recordsFromLayer: function() { //return table: key,value from layer + var retRecords = {}, + propName = this.options.propertyName, + loc; + + this._layer.eachLayer(function(layer) { + + if(layer instanceof SearchMarker) return; + + if(layer instanceof L.Marker) + { + if(layer.options.hasOwnProperty(propName)) + { + loc = layer.getLatLng(); + loc.layer = layer; + retRecords[ layer.options[propName] ] = loc; + + }else if(layer.feature.properties.hasOwnProperty(propName)){ + + loc = layer.getLatLng(); + loc.layer = layer; + retRecords[ layer.feature.properties[propName] ] = loc; + + }else{ + console.log("propertyName '"+propName+"' not found in marker", layer); + } + } + else if(layer.hasOwnProperty('feature'))//GeoJSON layer + { + if(layer.feature.properties.hasOwnProperty(propName)) + { + loc = layer.getBounds().getCenter(); + loc.layer = layer; + retRecords[ layer.feature.properties[propName] ] = loc; + } + else + console.log("propertyName '"+propName+"' not found in feature", layer); + } + + },this); + + return retRecords; + }, + + _autoType: function() { + + //TODO implements autype without selection(useful for mobile device) + + var start = this._input.value.length, + firstRecord = 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._handleArrowSelect(1); + this._handleSubmit(); //do search + break; + case 38://Up + this._handleArrowSelect(-1); + break; + case 40://Down + this._handleArrowSelect(1); + break; + case 37://Left + case 39://Right + case 16://Shift + case 17://Ctrl + //case 32://Space + break; + case 8://backspace + case 46://delete + this._autoTypeTmp = false;//disable temporarily autoType + 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(); + } + }, + + _fillRecordsCache: function() { +//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(callData,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 + + var inputText = this._input.value; + + L.DomUtil.addClass(this._container, 'search-load'); + + if(this.options.callData) //CUSTOM SEARCH CALLBACK(USUALLY FOR AJAX SEARCHING) + { + var that = this; + this.options.callData(inputText, function(jsonraw) { + + that._recordsCache = that._filterJSON(jsonraw); + + that.showTooltip(); + + L.DomUtil.removeClass(that._container, 'search-load'); + }); + } + else if(this.options.url) //JSONP/AJAX REQUEST + { + if(this.options.jsonpParam) + { + var that = this; + this._recordsFromJsonp(inputText, function(data) {// is async request then it need callback + that._recordsCache = data; + that.showTooltip(); + L.DomUtil.removeClass(that._container, 'search-load'); + }); + } + else + { + var that = this; + this._recordsFromAjax(inputText, function(data) {// is async request then it need callback + that._recordsCache = data; + that.showTooltip(); + L.DomUtil.removeClass(that._container, 'search-load'); + }); + } + } + else if(this.options.layer) //SEARCH ELEMENTS IN PRELOADED LAYER + { + this._recordsCache = this._recordsFromLayer(); //fill table key,value from markers into layer + this.showTooltip(); + L.DomUtil.removeClass(this._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= (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') { // regular up/down + 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 + }); + } + //this.collapse(); + //FIXME if collapse in _handleSubmit hide _markerLoc! + } + } + }, + + _getLocation: function(key) { //extract latlng from _recordsCache + + if( this._recordsCache.hasOwnProperty(key) ) + return this._recordsCache[key];//then after use .loc attribute + else + return false; + }, + + showLocation: function(latlng, title) { //set location on map from _recordsCache + + if(this.options.zoom) + this._map.setView(latlng, this.options.zoom); + else + this._map.panTo(latlng); + + if(this._markerLoc) + { + this._markerLoc.setLatLng(latlng); //show circle/marker in location found + this._markerLoc.setTitle(title); + this._markerLoc.show(); + if(this.options.animateLocation) + this._markerLoc.animate(); + //TODO showLocation: start animation after setView or panTo, maybe with map.on('moveend')... + } + + //FIXME autoCollapse option hide this._markerLoc before that visualized!! + if(this.options.autoCollapse) + this.collapse(); + return this; + } +}); + +var SearchMarker = L.Marker.extend({ + + includes: L.Mixin.Events, + + options: { + radius: 10, + weight: 3, + color: '#e03', + stroke: true, + fill: false, + title: '', + //TODO add custom icon! + marker: false //show icon optional, show only circleLoc + }, + + initialize: function (latlng, options) { + L.setOptions(this, options); + L.Marker.prototype.initialize.call(this, latlng, options); + this._circleLoc = new L.CircleMarker(latlng, this.options); + //TODO add inner circle + }, + + onAdd: function (map) { + L.Marker.prototype.onAdd.call(this, map); + map.addLayer(this._circleLoc); + this.hide(); + }, + + onRemove: function (map) { + L.Marker.prototype.onRemove.call(this, map); + map.removeLayer(this._circleLoc); + }, + + setLatLng: function (latlng) { + L.Marker.prototype.setLatLng.call(this, latlng); + this._circleLoc.setLatLng(latlng); + return this; + }, + + setTitle: function(title) { + title = title || ''; + this.options.title = title; + if(this._icon) + this._icon.title = title; + return this; + }, + + show: function() { + if(this.options.marker) + { + if(this._icon) + this._icon.style.display = 'block'; + if(this._shadow) + this._shadow.style.display = 'block'; + //this._bringToFront(); + } + if(this._circleLoc) + { + this._circleLoc.setStyle({fill: this.options.fill, stroke: this.options.stroke}); + //this._circleLoc.bringToFront(); + } + return this; + }, + + hide: function() { + if(this._icon) + this._icon.style.display = 'none'; + if(this._shadow) + this._shadow.style.display = 'none'; + if(this._circleLoc) + this._circleLoc.setStyle({fill: false, stroke: false}); + return this; + }, + + animate: function() { + //TODO refact animate() more smooth! like this: http://goo.gl/DDlRs + var circle = this._circleLoc, + tInt = 200, //time interval + ss = 10, //frames + mr = parseInt(circle._radius/ss), + oldrad = this.options.radius, + newrad = circle._radius * 2.5, + acc = 0; + + circle._timerAnimLoc = setInterval(function() { + acc += 0.5; + mr += acc; //adding acceleration + newrad -= mr; + + circle.setRadius(newrad); + + if(newrad