1/** Attaches autocomplete functionality to the input element 2 * Options is an object with these properties: 3 * source is either a static list of data, or function that returns a list (e.g. via ajax) of results. Each result should have at least these properties: label, value, desc 4 * select is a callback that takes a filtered result as arg, and can be run for side effects such as setting a marker on a map 5 * renderItem is a callback for rendering the menu items, it takes as arguments the menu element and item and should return a rendered list item. 6* Items are rendered by default with the label on a first line, and a smaller font description on the second. 7* 8* Where possible this file should not assume that cyclestreetsNS has been loaded. 9*/ 10/*jslint browser: true, devel: true, passfail: false, continue: true, eqeq: true, forin: true, nomen: true, plusplus: true, regexp: true, unparam: true, vars: true, white: true */ 11/*global jQuery, cyclestreetsNS, map, L */ 12 13var autocomplete = (function ($) { 14 'use strict'; 15 16 return { 17 18// Public methods 19 20 addTo: function (inputElement, options) { 21 22 var element = $(inputElement); 23 24 // Default options 25 if (options === undefined) {options = {};} 26 27 // Default options to the source url 28 if (typeof options === "string") {options = {sourceUrl: options};} 29 30 // Default to geocoder function 31 if (options.source === undefined) { 32 options.source = function (request, response) {autocomplete.geocoderAjax (element, options.sourceUrl, request, response);}; 33 } 34 35 // If there is a map on the page then default to centering on the selection 36 if (options.select === undefined) { 37 38 // Select callback - centres map on selection 39 options.select = autocomplete.centreMap; 40 } 41 42 // Default function for rendering the menu 43 if (options.renderItem === undefined) { 44 45 // Default render over two lines, second line smaller font size 46 options.renderItem = function( ul, item ) { 47 return $( "<li>" ) 48 .append( "<a>" + item.label + "<br /><span>" + item.desc + "</span></a>" ) 49 .appendTo( ul ); 50 }; 51 } 52 53 // Control caching of results 54 if (options.cacheResults !== undefined) { 55 56 // Create the cache or turn off caching 57 element.data('autocompleteCache', options.cacheResults ? {} : false); 58 } 59 60 // Run on document ready 61 $(function () { 62 63 // Disable enter key when focus is on input element itself (i.e. not on a drop-down value) 64 $(inputElement).keypress(function(e) { 65 var code = e.keyCode || e.which; 66 if (code === 13) {return false;} 67 }); 68 69 // Add the autocomplete to the element 70 $(inputElement).autocomplete({ 71 appendTo: '#geocoder', // Ensures this is within the CSS scopes defined by the application, rather than being attached to the surrounding GUI's body which may have different CSS 72 source: options.source, 73 minLength: 3, 74 focus: function (event, ui) { 75 76 // This is called when the focussed menu item changes, show the label (rather than the value) in the input element 77 $(inputElement).val(ui.item.label); 78 79 // Prevent the widget from inserting the value. 80 return false; 81 }, 82 select: function (event, ui) { 83 84 // Apply any select callback 85 if (options.select !== undefined) {return options.select (event, ui);} 86 87 // Set the value to the label 88 $(inputElement).val(ui.item.label); 89 90 // Prevent the widget from inserting the value. 91 return false; 92 } 93 }).data("ui-autocomplete")._renderItem = options.renderItem; 94 }); 95 }, 96 97 98 // Centres map on lon lat in the autocomplete result 99 centreMap: function (event, ui) { 100 101 // Declarations 102 var result = ui.item; 103 104 // Abandon if no map 105 if (cyclestreetsNS === undefined || cyclestreetsNS.map === undefined) {return;} 106 107 // Zoom to fit the bounds of the polygon 108 var geojsonItemLayer = L.geoJson(ui.item.feature); 109 cyclestreetsNS.map.fitBounds(geojsonItemLayer.getBounds()); 110 111 // Zoom out if now too close 112 if (cyclestreetsNS.map.getZoom() > 17) { 113 cyclestreetsNS.map.setZoom(17); 114 } 115 }, 116 117 118// Private methods 119 120 // Function used to retrieve results from a Nominatim server 121 geocoderAjax: function (element, url, request, response) { 122 123 // The url should already include the api key, this object c
123ollates additional query string arguments for the CycleStreets geocoder api call 124 // http://www.cyclestreets.net/api/v2/geocoder/ 125 var urlParams = { 126 q: request.term, // I.e. the search string 127 limit: 12 // Ideally this and perhaps other parameters that are sent to the geocoder could be settings 128 }, 129 130 // Useful var to help determine any bounding box for the geocoding search 131 bounds, 132 133 // Bind the term for use in caching 134 term = request.term, 135 136 // Does the search term include a comma, which can be used in searches to specify particular places, such as other countries 137 containsComma = /,/.test(term), 138 139 // Cache is stored with the element 140 cache = element.data('autocompleteCache'), 141 142 // Flag to detect if the term has been found in the cache 143 foundInCache = false; 144 145 // When caching is enabled for this element 146 if (cache) { 147 148 // Scan the cache for data matching the term 149 $.each(cache, function (key, data) { 150 151 // Skip if no match 152 if (term !== key) {return true;} 153 154 // Apply the response callback 155 response(data); 156 157 // Flag found 158 foundInCache = true; 159 160 // End the search 161 return false; 162 }); 163 } 164 165 // End if the result has been found in the cache 166 if (foundInCache) return; 167 168 // Get any preferred bounds - they are packed as W,S,E,N 169 if (window.cyclestreetsNS !== undefined && cyclestreetsNS.getPreferedNameSearchBoundingBox()) { 170 171 // Set as bbox WSEN 172 urlParams.bbox = cyclestreetsNS.getPreferedNameSearchBoundingBox(); 173 } 174 175 // If there is a map on the page then assume it will provide bounds 176 else if ($("#map")) { 177 178 // Callback that returns a csv: <left>,<top>,<right>,<bottom> 179 urlParams.bbox = autocomplete.mapBoundsCallBack(); 180 } 181 182 // Country codes (commas present in search string skip the country restriction) 183 if (!containsComma && window.cyclestreetsNS !== undefined && cyclestreetsNS.getGeocodingCountryCodes()) {urlParams.countrycodes = cyclestreetsNS.getGeocodingCountryCodes();} 184 // #!# Hotfix - needs to be dealt with upstream properly in the library: 185 urlParams.countrycodes = 'gb,ie'; 186 187 // Bounded 188 if (window.cyclestreetsNS !== undefined && cyclestreetsNS.getBoundedSearch()) {urlParams.bounded = cyclestreetsNS.getBoundedSearch();} 189 190 $.ajax({ 191 url: url, 192 193 // The next two options configure jQuery to parse the jsonp returned by call 194 dataType: (window.cyclestreetsNS !== undefined && cyclestreetsNS.getUseJsonpTransport() ? 'jsonp' : 'json'), 195 // Nominatim supports the value of this parameter which wraps json output in a callback function. 196 // The callback function name is generated by jQuery, and parses the json. 197 jsonp: (window.cyclestreetsNS !== undefined && cyclestreetsNS.getUseJsonpTransport() ? 'callback' : null), 198 199 data: urlParams, 200 201 // Provides a useful chance to show the api request url 202 beforeSend: function (jqXHR, settings) { 203 204 // Skip cyclestreetsNS not present 205 if (window.cyclestreetsNS === undefined) {return;} 206 207 // Show the requested url 208 cyclestreetsNS.debugtagmsg(settings.url); 209 }, 210 211 // On success run returned data through a filter 212 success: function (data, textStatus, jqXHR) { 213 214 // Declarations 215 var results = []; 216 217 // No data 218 if (!data) { 219 220 // No results 221 results = []; 222 223 } else if (data.error) { 224 225 // By using these values the user can see that an error has occurred 226 results = [{ 227 value: false, 228 label: 'A geocoding error occurred', 229 desc: data.error}]; 230 231 } else if (!data.features) { 232 233 // No results 234 results = []; 235 236 } else { 237 238 // Render each feature 239 results = $.map(data.features, autocomplete.renderGeocoderResult); 240 } 241 242 // Cache the results in the element 243 if (cache) { 244 cache[term] = results; 245 element.data('autocompleteCache', cache); 246 } 247 248 // Apply the response callback 249 response (results); 250 }, 251 252 // Autocomplete needs to call response() even when an error occurs to avoid falling into an invali
252d state. 253 error: function (jqXHR, textStatus, errorThrown) { 254 255 // By using these values the user can see that an error has occurred 256 response ([{ 257 value: errorThrown, 258 label: "An error occurred: " + textStatus, 259 desc: "Error type: " + errorThrown}]); 260 }, 261 262 // Handle some http error codes 263 statusCode: { 264 404: function () { 265 alert( "The name lookup service cannot be found" ); 266 } 267 } 268 }); 269 }, 270 271 272 // Bounds callback - retrieves current bbox of map 273 mapBoundsCallBack: function () { 274 275 // Declarations 276 var bounds; 277 278 // Abandon if no map 279 if (window.cyclestreetsNS === undefined || cyclestreetsNS.map === undefined) {return;} 280 281 // Visible bounds of the map 282 bounds = cyclestreetsNS.map.getBounds(); 283 284 // Expand the bounds to be as least as big as a city 285 bounds = this.atLeastAsBigAsACity (bounds); 286 287 // Return WSEN 288 return bounds.toBBoxString(); 289 }, 290 291 292 // Expand the bounds to be as least as big as a city 293 atLeastAsBigAsACity: function (bounds) 294 { 295 // Declarations 296 var centre = bounds.getCenter(), 297 298 // City size in degrees NS, 1 degree = 100km, so 0.1 degree = 10km. 299 sizeNSdegrees = 0.25, 300 301 // Accommodate the forshortening of longitude 302 sizeEWdegrees = sizeNSdegrees / Math.cos(centre.lat * Math.PI / 180); 303 304 // Extend the bounds to the NW 305 bounds = bounds.extend (L.latLng(centre.lat + sizeNSdegrees, centre.lng - sizeEWdegrees)); 306 307 // Extend the bounds to the SE 308 bounds = bounds.extend (L.latLng(centre.lat - sizeNSdegrees, centre.lng + sizeEWdegrees)); 309 310 // Result 311 return bounds; 312 }, 313 314 // Used to process each result from the geocoder 315 renderGeocoderResult: function (item) 316 { 317 // Form result object 318 var result = { 319 value: item.properties.name + ', ' + item.properties.near, // Feeding it back into the query seems to be the best way of getting a repeatable result. 320 label: item.properties.name, 321 desc: item.properties.near, 322 lat: item.geometry.coordinates[1], 323 lon: item.geometry.coordinates[0], 324 feature: item 325 }; 326 327 // Result 328 return result; 329 } 330 }; 331}(jQuery)); 332
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.