1 /** 2 * @fileOverview 3 * Copyright (c) 2013 Regione Autonoma della Sardegna 4 * Published under the GPL license.<br> 5 * See <a href="https://sardegnageoportale.it/webgis/license.txt">https://sardegnageoportale.it/webgis/license.txt</a> 6 * for the full text of the license. 7 * @version 0.1 8 */ 9 10 /** 11 * @require OpenLayers/Control/SelectFeature.js 12 */ 13 14 /** 15 * @namespace framework.plugins 16 */ 17 Ext.namespace("framework.form"); 18 19 /** 20 * Creates a combo box that handles results from a geocoding service. By 21 * default it uses Directory OpenLS service by Regione Autonoma della Sardegna (http://www.sardegnageoportale.it/index.php?xsl=1598&s=203310&v=2&c=9869&t=1), but it can be configured with a custom store 22 * to use other services standard OpenLS. If the user enters a valid address in the search 23 * box, the combo's store will be populated with records that match the 24 * address. 25 * @name_ GeocoderOpenLSComboBox 26 * @class Creates a combo box that handles results from a geocoding service OpenLS. 27 * @constructor 28 @extends <a target="_blank" href="http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.form.ComboBox">Ext.form.ComboBox</a> 29 */ 30 framework.form.GeocoderOpenLSComboBox = Ext.extend(Ext.form.ComboBox, 31 /** 32 * @lends framework.form.GeocoderOpenLSComboBox.prototype 33 */ 34 { 35 /** 36 * Text to display for an empty field 37 * @public 38 * @type String 39 */ 40 emptyText: "Search with OpenLS", 41 /** 42 * The srs used by the geocoder service. 43 * Default is "EPSG:3003". 44 * @public 45 * @type String 46 */ 47 srs: "EPSG:4326", 48 /** 49 * The minimum zoom level to use when zooming to a location. 50 * Not used when zooming to a bounding box. Default is 12. 51 * @public 52 * @type number 53 */ 54 zoom: 12, 55 /** 56 * The minimum zoom level to use when zooming to a municipality. 57 * Not used when zooming to a bounding box. Default is 8. 58 * @public 59 * @type number 60 */ 61 zoomMunicipality: 8, 62 /** 63 * If provided, a marker will be drawn on this 64 * layer with the location returned by the geocoder. The location will be 65 * cleared when the map panned. 66 * @public 67 * @type OpenLayers.Layer.Vector 68 */ 69 layer: null, 70 /** 71 * style point result 72 * @public 73 * @type OpenLayers.Layer.Vector 74 */ 75 vectorStyle: null, 76 /** 77 * Delay before the search occurs. 78 * Default is 200ms. 79 * @public 80 * @type number 81 */ 82 queryDelay: 200, 83 /** 84 * Field from selected record to use when the combo's 85 * :meth:`getValue` method is called. Default is "bounds". This field is 86 * supposed to contain an array of [left, bottom, right, top] coordinates 87 * for a bounding box or [x, y] for a location. 88 * @public 89 * @type String 90 */ 91 valueField: "bounds", 92 /** 93 * The field to display in the combo boy. Default is 94 * "name" for instant use with the default store for this component. 95 * @public 96 * @type String 97 */ 98 displayField: "text", 99 /** 100 * The field to get the location from. This field is supposed 101 * to contain an array of [x, y] for a location. Default is "lonlat" for 102 * instant use with the default store for this component. 103 * @public 104 * @type String 105 */ 106 locationField: "lonlat", 107 /** URL template for querying the geocoding service. If a 108 * :obj:`store` is configured, this will be ignored. Note that the 109 * :obj:`queryParam` will be used to append the user's combo box 110 * input to the url. Default is 111 * "http://nominatim.openstreetmap.org/search?format=json", for instant 112 * use with the OSM Nominatim geolocator. However, if you intend to use 113 * that, note the 114 * `Nominatim Usage Policy <http://wiki.openstreetmap.org/wiki/Nominatim_usage_policy>`_. 115 * @public 116 * @type String 117 */ 118 url: 'http://webgis.regione.sardegna.it/followmeplus/OLService?serviceName=locationservice&exactStreetName=false', 119 /** 120 * The query parameter for the user entered search text. 121 * Default is "q" for instant use with OSM Nominatim. 122 * @public 123 * @type String 124 */ 125 queryParam: "xmlRequest", 126 /** 127 * Minimum number of entered characters to trigger a search. 128 * Default is 4. 129 * @public 130 * @type Number 131 */ 132 minChars: 4, 133 /** 134 * See http://www.dev.sencha.com/deploy/dev/docs/source/BoxComponent.html#cfg-Ext.BoxComponent-width, 135 * default value is 240. 136 * @public 137 * @type Number 138 */ 139 width: 240, 140 /** 141 * See http://www.dev.sencha.com/deploy/dev/docs/source/Combo.html#cfg-Ext.form.ComboBox-listWidth, 142 * default value is 350. 143 * @public 144 * @type Number 145 */ 146 listWidth: 400, 147 /** 148 * See http://www.dev.sencha.com/deploy/dev/docs/source/Combo.html#cfg-Ext.form.ComboBox-loadingText, 149 * default value is "Search in Geozet...". 150 * @public 151 * @type String 152 */ 153 loadingText: 'Searching...', 154 /** 155 * Hide trigger of the combo. 156 * @private 157 * @type boolean 158 */ 159 hideTrigger: true, 160 /** 161 * Force selection. 162 * @private 163 * @type boolean 164 */ 165 forceSelection: false, 166 /** private: property[countryCode] 167 * countryCode, default is IT. 168 * @private 169 * @type String 170 */ 171 countryCode: 'IT', 172 173 textSearch: '', 174 175 /** 176 * Override init component 177 * @private 178 */ 179 initComponent: function() { 180 181 var epsgOutput = this.srs.replace("EPSG:", ""); 182 183 if (!this.map) { 184 var mapPanel = this.findParentBy(function(cmp) { 185 return cmp instanceof GeoExt.MapPanel; 186 }); 187 this.map = mapPanel.map; 188 } 189 if (Ext.isString(this.srs)) { 190 this.srs = new OpenLayers.Projection(this.srs); 191 } 192 if (this.proxy) 193 var sUrl = this.proxy + escape(this.url + '&epsgOutput=' + epsgOutput); 194 else 195 var sUrl = this.url + '&epsgOutput=' + epsgOutput; 196 if (!this.store) { 197 this.store = new Ext.data.Store({ 198 proxy: new Ext.data.HttpProxy({ 199 url: sUrl, 200 method: 'POST' 201 }), 202 fields: [ 203 {name: "lon", type: "number"}, 204 {name: "lat", type: "number"}, 205 "text", 206 "xml", 207 "type" 208 ], 209 reader: new framework.data.GeocoderOpenLS_XLSReader() 210 }); 211 } 212 213 this.on({ 214 added: this.handleAdded, 215 select: this.handleSelect, 216 focus: function() { 217 this.clearResult(); 218 if (this.store.data.items.length > 0) 219 this.expand(); 220 this.setValue(this.textSearch); 221 // this.removeLocationFeature(); 222 }, 223 blur: function(textfield) { 224 // textfield.setValue(""); 225 textfield.setValue(this.textSearch); 226 }, 227 keyup: function(textfield, eventObject) { 228 this.textSearch = this.el.dom.value; 229 if (eventObject.getCharCode() == Ext.EventObject.ENTER) { 230 this.focus(); 231 this.setValue(this.textSearch); 232 this.doQuery(this.el.dom.value, true); 233 } else { 234 if (this.isExpanded()) { 235 this.list.hide(); 236 } 237 } 238 }, 239 scope: this 240 }); 241 242 if (this.layer) { 243 244 this.map.addLayer(this.layer); 245 246 this.selectCtrl = new OpenLayers.Control.SelectFeature([this.layer], { 247 toggle: true, 248 clickout: true 249 }); 250 251 this.map.addControl(this.selectCtrl); 252 this.selectCtrl.activate(); 253 } 254 255 256 return framework.form.GeocoderOpenLSComboBox.superclass.initComponent.apply(this, arguments); 257 }, 258 /** 259 * When this component is added to a container, see if it has a parent 260 * MapPanel somewhere and set the map 261 * @private 262 */ 263 handleAdded: function() { 264 var mapPanel = this.findParentBy(function(cmp) { 265 return cmp instanceof GeoExt.MapPanel; 266 }); 267 if (mapPanel) { 268 this.setMap(mapPanel); 269 } 270 271 }, 272 /** 273 * Hides the dropdown list if it is currently expanded. Fires the {@link #collapse} event on completion. 274 * @private 275 */ 276 collapse: function() { 277 if (!this.isExpanded()) { 278 return; 279 } 280 this.list.hide(); 281 this.el.blur(); 282 Ext.getDoc().un('mousewheel', this.collapseIf, this); 283 Ext.getDoc().un('mousedown', this.collapseIf, this); 284 this.fireEvent('collapse', this); 285 }, 286 /** 287 * Zoom to the selected location, and also set a location marker if this 288 * component was configured with an :obj:`layer`. 289 * @private 290 * @param {Object} combo comnoBox object. 291 * @param {Object} record selected record. 292 * @param {Object} idx index of selected record. 293 */ 294 handleSelect: function(combo, record, index) { 295 296 if(record.data.lon == 0 && record.data.lat == 0){ 297 return; 298 } 299 300 if (this.layer) { 301 302 if (this.map.getLayerIndex(this.layer) == -1) { 303 this.map.addLayer(this.layer) 304 } 305 306 var controlosSelectFeature = this.map.getControlsByClass("OpenLayers.Control.SelectFeature"); 307 if(!controlosSelectFeature || controlosSelectFeature.length == 0){ 308 309 this.selectCtrl = new OpenLayers.Control.SelectFeature([this.layer], { 310 toggle: true, 311 clickout: true 312 }); 313 314 this.map.addControl(this.selectCtrl); 315 this.selectCtrl.activate(); 316 } 317 } 318 319 //strip HTML tags from string 320 var div = document.createElement("div"); 321 div.innerHTML = record.data.text; 322 var text = div.textContent || div.innerText || ""; 323 324 this.setValue(text); // put the selected name in the box 325 326 327 var position = new OpenLayers.LonLat(record.data.lon, record.data.lat); 328 329 // Reproject (if required) 330 position.transform( 331 new OpenLayers.Projection(this.srs.projCode), 332 this.map.getProjectionObject() 333 ); 334 335 // zoom in on the location 336 if (this.zoom) { 337 var zoomResult = null; 338 if (record.data.type == "municipality") 339 zoomResult = this.zoomMunicipality; 340 else 341 zoomResult = this.zoom; 342 343 this.map.setCenter(position, zoomResult); 344 } 345 346 //insert marker 347 348 if (this.layer) { 349 //remove marker and popup 350 this.layer.destroyFeatures(); 351 removePopup(); 352 353 var feature = new OpenLayers.Feature.Vector( 354 new OpenLayers.Geometry.Point(position.lon, position.lat), 355 null, 356 this.vectorStyle); 357 358 feature.text = record.data.text; 359 360 this.layer.addFeatures(feature); 361 362 createPopup(feature); 363 364 this.map.searchName = this.id; 365 366 // create popup on "featureselected" 367 this.layer.events.on({ 368 featureselected: function(e) { 369 createPopup(e.feature); 370 } 371 }); 372 } 373 374 function createPopup(feature) { 375 removePopup(); 376 var feature_ = feature; 377 this.popupAddr = new GeoExt.Popup({ 378 //title: 'Ricerca indirizzo', 379 //map: this.map, 380 location: feature, 381 width: 200, 382 border: false, 383 html: '<div class="popupAddr">' + feature.text + '</div>', 384 maximizable: false, 385 collapsible: false 386 }); 387 // // unselect feature when the popup 388 // // is closed 389 // this.popupAddr.on({ 390 // close: function() { 391 // if (OpenLayers.Util.indexOf(feature_.layer.selectedFeatures, feature_) > -1) { 392 // selectCtrl.unselect(feature_); 393 // } 394 // }, 395 // scope: this 396 // }); 397 this.popupAddr.show(); 398 } 399 400 401 function removePopup() { 402 if (this.popupAddr) { 403 this.popupAddr.close(); 404 this.popupAddr.destroy(); 405 } 406 407 } 408 409 // blur the combo box 410 //TODO Investigate if there is a more elegant way to do this. 411 (function() { 412 this.triggerBlur(); 413 this.el.blur(); 414 }).defer(100, this); 415 }, 416 /** 417 * Handler for the map's moveend event. Clears the selected location 418 * when the map center has changed. 419 * @private 420 */ 421 clearResult: function() { 422 this.clearValue(); 423 }, 424 /** 425 * Set the :obj:`map` for this instance. 426 * @private 427 * @param {GeoExt.MapPanel||OpenLayers.Map} map object 428 */ 429 setMap: function(map) { 430 if (map instanceof GeoExt.MapPanel) { 431 map = map.map; 432 } 433 this.map = map; 434 map.events.on({ 435 // "moveend": this.clearResult, 436 // "click": this.removeFocus, 437 scope: this 438 }); 439 }, 440 /** 441 * Called by a MapPanel if this component is one of the items in the panel. 442 * @private 443 */ 444 addToMapPanel: Ext.emptyFn, 445 446 /** 447 * Execute a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the 448 * query allowing the query action to be canceled if needed. 449 * @private 450 * @param {String} query The SQL query to execute 451 * @param {Boolean} forceAll <tt>true</tt> to force the query to execute even if there are currently fewer 452 * characters in the field than the minimum specified by the <tt>{@link #minChars}</tt> config option. It 453 * also clears any filter previously saved in the current store (defaults to <tt>false</tt>) 454 */ 455 doQuery: function(q, forceAll) { 456 qc = Ext.isEmpty(q) ? '' : q; 457 q = Ext.isEmpty(q) ? '' : '<GeocodeRequest xmlns="http://www.opengis.net/xls">'+ 458 '<Address countryCode="' + this.countryCode + '">'+ 459 '<freeFormAddress>' + q + '</freeFormAddress>'+ 460 '</Address>'+ 461 '</GeocodeRequest>'; 462 var qe = { 463 query: q, 464 forceAll: forceAll, 465 combo: this, 466 cancel: false 467 }; 468 if (this.fireEvent('beforequery', qe) === false || qe.cancel) { 469 return false; 470 } 471 q = qe.query; 472 forceAll = qe.forceAll; 473 if (forceAll === true || (qc.length >= this.minChars)) { 474 if (this.lastQuery !== q) { 475 this.lastQuery = q; 476 if (this.mode == 'local') { 477 this.selectedIndex = -1; 478 if (forceAll) { 479 this.store.clearFilter(); 480 } else { 481 this.store.filter(this.displayField, q); 482 } 483 this.onLoad(); 484 } else { 485 this.store.baseParams[this.queryParam] = q; 486 this.store.load({ 487 params: this.getParams(q) 488 }); 489 this.expand(); 490 } 491 } else { 492 this.selectedIndex = -1; 493 this.onLoad(); 494 } 495 } 496 } 497 498 499 }); 500 501 /** api: xtype = gx_geocoderopenlscombobox */ 502 Ext.reg("framework_geocoderopenlscombobox", framework.form.GeocoderOpenLSComboBox); 503