summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Franke <nobody@nowhere.ws>2013-12-12 16:10:32 +0100
committerChristian Franke <nobody@nowhere.ws>2013-12-12 16:10:32 +0100
commitfb2bde41ac04ee93c4dd238dcb3e812ac2d15356 (patch)
treec7151cf7a7e1a0cd621c1efd692d373e71affdbd
parentb42134c08c24a14407e06e6b559a2370999c9e7f (diff)
Add search functionality
-rw-r--r--web/css/leaflet.css2
-rw-r--r--web/css/leaflet.search.css134
-rw-r--r--web/index.html2
-rw-r--r--web/js/eventmap.js52
-rw-r--r--web/js/leaflet.search.js847
5 files changed, 1035 insertions, 2 deletions
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 @@
<link rel="stylesheet" href="css/leaflet.contextmenu.css" />
<link rel="stylesheet" href="css/leaflet.draw.css" />
<link rel="stylesheet" href="css/leaflet.label.css" />
+ <link rel="stylesheet" href="css/leaflet.search.css" />
<link rel="stylesheet" href="css/eventmap.css" />
</head>
<body>
@@ -20,6 +21,7 @@
<script src="js/leaflet.contextmenu.js"></script>
<script src="js/leaflet.draw.js"></script>
<script src="js/leaflet.label.js"></script>
+ <script src="js/leaflet.search.js"></script>
<script src="js/eventmap.js"></script>
</body>
</html>
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 = "<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('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<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') { // 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<oldrad)
+ {
+ clearInterval(circle._timerAnimLoc);
+ circle.setRadius(oldrad);//reset radius
+ //if(typeof afterAnimCall == 'function')
+ //afterAnimCall();
+ //TODO use create event 'animateEnd' in SearchMarker
+ }
+ }, 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);
+};
+
+}).call(this);
+