diff --git a/assets/src/modules/Search.js b/assets/src/modules/Search.js
index 0885778aa8..547f2f32f6 100644
--- a/assets/src/modules/Search.js
+++ b/assets/src/modules/Search.js
@@ -5,6 +5,8 @@
* @license MPL-2.0
*/
+const AUTOCOMPLETE_MIN_LENGTH = 3;
+
/**
* @class
* @name Search
@@ -55,6 +57,20 @@ export default class Search {
$('#lizmap-search, #lizmap-search-close').removeClass('open');
}
+ /**
+ * Returns a debounced version of fn that fires after delay ms of inactivity
+ * @param {Function} fn
+ * @param {number} delay - milliseconds
+ * @returns {Function}
+ */
+ _debounce(fn, delay) {
+ let timer;
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => fn(...args), delay);
+ };
+ }
+
/**
* Start the external search
*/
@@ -94,10 +110,57 @@ export default class Search {
}
/**
- * PRIVATE method: addExternalSearch
- * add external search capability
+ * PRIVATE method: fire a lizmapFts search request and display results
* @param {object} searchConfig - search configuration
- * @returns {boolean} external search is in the user interface
+ * @param {OpenLayers.Bounds} extent - WGS84 map extent for filtering
+ */
+ _performFtsSearch(searchConfig, extent) {
+ this._startExternalSearch();
+
+ var labrex = this._getHighlightRegEx();
+ $.get(searchConfig.url
+ , {
+ "repository": globalThis['lizUrls'].params.repository,
+ "project": globalThis['lizUrls'].params.project,
+ "query": $('#search-query').val(),
+ }
+ , (results) => {
+ var text = '';
+ var count = 0;
+
+ for (var ftsId in results) {
+ var ftsLayerResult = results[ftsId];
+ text += '
' + ftsLayerResult.search_name + '';
+ text += '';
+ for (var i = 0, len = ftsLayerResult.features.length; i < len; i++) {
+ var ftsFeat = ftsLayerResult.features[i];
+ var ftsGeometry = OpenLayers.Geometry.fromWKT(ftsFeat.geometry);
+ if (ftsLayerResult.srid != 'EPSG:4326') {
+ ftsGeometry.transform(ftsLayerResult.srid, 'EPSG:4326');
+ }
+ var bbox = ftsGeometry.getBounds();
+ if (extent.intersectsBounds(bbox)) {
+ var lab = ftsFeat.label.replace(labrex, '$1');
+ text += '- ' + lab + '
';
+ count++;
+ }
+ }
+ text += '
';
+ }
+
+ if (count != 0 && text != '') {
+ this._updateExternalSearch(text);
+ }
+ else {
+ this._updateExternalSearch('' + lizDict['externalsearch.mapdata'] + '- ' + lizDict['externalsearch.notfound'] + '
');
+ }
+ }, 'json');
+ }
+
+ /**
+ * PRIVATE method: add lizmapFts search capability with autocomplete
+ * @param {object} searchConfig - search configuration
+ * @returns {boolean} search is in the user interface
*/
_addSearch(searchConfig) {
if (searchConfig.type == 'externalSearch') {
@@ -112,58 +175,158 @@ export default class Search {
var extent = new OpenLayers.Bounds(this._lizmap3.map.maxExtent.toArray());
extent.transform(this._map.getView().getProjection().getCode(), wgs84);
+ const autoSearch = () => {
+ if ($('#search-query').val().length < AUTOCOMPLETE_MIN_LENGTH) {
+ $('#lizmap-search .items').html('');
+ $('#lizmap-search, #lizmap-search-close').removeClass('open');
+ return;
+ }
+ this._performFtsSearch(searchConfig, extent);
+ };
+
$('#nominatim-search').submit(() => {
- this._startExternalSearch();
-
- // Format answers to highlight searched keywords
- var labrex = this._getHighlightRegEx();
- $.get(searchConfig.url
- , {
- "repository": globalThis['lizUrls'].params.repository,
- "project": globalThis['lizUrls'].params.project,
- "query": $('#search-query').val(),
- }
- , (results) => {
- var text = '';
- var count = 0;
-
- // Loop through results
- for (var ftsId in results) {
- var ftsLayerResult = results[ftsId];
- text += '' + ftsLayerResult.search_name + '';
- text += '';
- for (var i = 0, len = ftsLayerResult.features.length; i < len; i++) {
- var ftsFeat = ftsLayerResult.features[i];
- var ftsGeometry = OpenLayers.Geometry.fromWKT(ftsFeat.geometry);
- if (ftsLayerResult.srid != 'EPSG:4326') {
- ftsGeometry.transform(ftsLayerResult.srid, 'EPSG:4326');
+ this._performFtsSearch(searchConfig, extent);
+ return false;
+ });
+
+ $('#search-query').on('input', this._debounce(autoSearch, 100));
+
+ return true;
+ }
+
+ /**
+ * PRIVATE method: fire an external geocoder search request and display results
+ * @param {object} searchConfig - search configuration
+ * @param {string|object} service - resolved service URL or Google Geocoder instance
+ * @param {OpenLayers.Bounds} extent - WGS84 map extent for filtering
+ */
+ _performExternalSearch(searchConfig, service, extent) {
+ this._startExternalSearch();
+
+ var labrex = this._getHighlightRegEx();
+ const searchQuery = document.getElementById('search-query').value;
+ switch (searchConfig.service) {
+ case 'nominatim':
+ $.get(service
+ , { "query": searchQuery, "bbox": extent.toBBOX() }
+ , data => {
+ var text = '';
+ var count = 0;
+ for (const address of data) {
+ if (count > 9) {
+ return false;
+ }
+ if (!address.boundingbox) {
+ return true;
}
- var bbox = ftsGeometry.getBounds();
+
+ var bbox = [
+ address.boundingbox[2],
+ address.boundingbox[0],
+ address.boundingbox[3],
+ address.boundingbox[1]
+ ];
+ bbox = new OpenLayers.Bounds(bbox);
if (extent.intersectsBounds(bbox)) {
- var lab = ftsFeat.label.replace(labrex, '$1');
- text += '- ' + lab + '
';
+ var lab = address.display_name.replace(labrex, '$1');
+ text += `- ${lab}
`;
count++;
}
}
- text += '
';
- }
-
- if (count != 0 && text != '') {
- this._updateExternalSearch(text);
+ if (count == 0 || text == '') {
+ text = '' + lizDict['externalsearch.notfound'] + '';
+ }
+ this._updateExternalSearch('OpenStreetMap');
+ }, 'json');
+ break;
+ case 'ign': {
+ if (searchQuery.length < 3 || searchQuery.length > 200) {
+ lizMap.addMessage(lizDict['externalsearch.ignlimit'], 'warning', true);
+ break;
+ }
+ $.get(service
+ , {
+ "text": searchQuery,
+ "type": 'StreetAddress',
+ "maximumResponses": 10,
+ "bbox": extent.toBBOX()
}
- else {
- this._updateExternalSearch('' + lizDict['externalsearch.mapdata'] + '- ' + lizDict['externalsearch.notfound'] + '
');
+ , data => {
+ let text = '';
+ let count = 0;
+ for (const result of data.results) {
+ var lab = result.fulltext.replace(labrex, '$1');
+ text += `
+
+
+ ${lab}
+
+ `;
+ count++;
+ }
+ if (count == 0 || text == '') {
+ text = '' + lizDict['externalsearch.notfound'] + '';
+ }
+ this._updateExternalSearch('IGN');
+ }, 'json');
+ break;
+ }
+ case 'google':
+ service.geocode({
+ 'address': searchQuery,
+ 'bounds': new google.maps.LatLngBounds(
+ new google.maps.LatLng(extent.top, extent.left),
+ new google.maps.LatLng(extent.bottom, extent.right)
+ )
+ }, (results, status) => {
+ if (status == google.maps.GeocoderStatus.OK) {
+ var text = '';
+ var count = 0;
+ for (const address of results) {
+ if (count > 9) {
+ return false;
+ }
+ var bbox = [];
+ if (address.geometry.viewport) {
+ bbox = [
+ address.geometry.viewport.getSouthWest().lng(),
+ address.geometry.viewport.getSouthWest().lat(),
+ address.geometry.viewport.getNorthEast().lng(),
+ address.geometry.viewport.getNorthEast().lat()
+ ];
+ } else if (address.geometry.bounds) {
+ bbox = [
+ address.geometry.bounds.getSouthWest().lng(),
+ address.geometry.bounds.getSouthWest().lat(),
+ address.geometry.bounds.getNorthEast().lng(),
+ address.geometry.bounds.getNorthEast().lat()
+ ];
+ }
+ if (bbox.length != 4) {
+ return false;
+ }
+ bbox = new OpenLayers.Bounds(bbox);
+ if (extent.intersectsBounds(bbox)) {
+ var lab = address.formatted_address.replace(labrex, '$1');
+ var wkt = 'POINT(' + address.geometry.location.lng() + ' ' + address.geometry.location.lat() + ')';
+ text += '' + lab + '';
+ count++;
+ }
+ }
+ if (count == 0 || text == '') {
+ text = '' + lizDict['externalsearch.notfound'] + '';
+ }
+ this._updateExternalSearch('Google');
+ } else {
+ this._updateExternalSearch('Google- ' + lizDict['externalsearch.notfound'] + '
');
}
- }, 'json');
- return false;
- });
-
- return true;
+ });
+ break;
+ }
}
/**
- * PRIVATE method: addExternalSearch
- * add external search capability
+ * PRIVATE method: add external geocoder search capability with autocomplete
* @param {object} searchConfig - search configuration
* @returns {boolean} external search is in the user interface
*/
@@ -201,133 +364,22 @@ export default class Search {
return false;
}
- $('#nominatim-search').submit(() => {
- this._startExternalSearch();
-
- // Format answers to highlight searched keywords
- var labrex = this._getHighlightRegEx();
- const searchQuery = document.getElementById('search-query').value;
- switch (searchConfig.service) {
- case 'nominatim':
- $.get(service
- , { "query": searchQuery, "bbox": extent.toBBOX() }
- , data => {
- var text = '';
- var count = 0;
- for (const address of data) {
- if (count > 9) {
- return false;
- }
- if (!address.boundingbox) {
- return true;
- }
-
- var bbox = [
- address.boundingbox[2],
- address.boundingbox[0],
- address.boundingbox[3],
- address.boundingbox[1]
- ];
- bbox = new OpenLayers.Bounds(bbox);
- if (extent.intersectsBounds(bbox)) {
- var lab = address.display_name.replace(labrex, '$1');
- text += `${lab}`;
- count++;
- }
- }
- if (count == 0 || text == '') {
- text = '' + lizDict['externalsearch.notfound'] + '';
- }
- this._updateExternalSearch('OpenStreetMap');
- }, 'json');
- break;
- case 'ign': {
- if (searchQuery.length < 3 || searchQuery.length > 200) {
- lizMap.addMessage(lizDict['externalsearch.ignlimit'], 'warning', true);
- break;
- }
- $.get(service
- , {
- "text": searchQuery,
- "type": 'StreetAddress',
- "maximumResponses": 10,
- "bbox": extent.toBBOX()
- }
- , data => {
- let text = '';
- let count = 0;
- for (const result of data.results) {
- var lab = result.fulltext.replace(labrex, '$1');
- text += `
-
-
- ${lab}
-
- `;
- count++;
- }
- if (count == 0 || text == '') {
- text = '' + lizDict['externalsearch.notfound'] + '';
- }
- this._updateExternalSearch('IGN');
- }, 'json');
- break;
- }
- case 'google':
- service.geocode({
- 'address': searchQuery,
- 'bounds': new google.maps.LatLngBounds(
- new google.maps.LatLng(extent.top, extent.left),
- new google.maps.LatLng(extent.bottom, extent.right)
- )
- }, (results, status) => {
- if (status == google.maps.GeocoderStatus.OK) {
- var text = '';
- var count = 0;
- for (const address of results) {
- if (count > 9) {
- return false;
- }
- var bbox = [];
- if (address.geometry.viewport) {
- bbox = [
- address.geometry.viewport.getSouthWest().lng(),
- address.geometry.viewport.getSouthWest().lat(),
- address.geometry.viewport.getNorthEast().lng(),
- address.geometry.viewport.getNorthEast().lat()
- ];
- } else if (address.geometry.bounds) {
- bbox = [
- address.geometry.bounds.getSouthWest().lng(),
- address.geometry.bounds.getSouthWest().lat(),
- address.geometry.bounds.getNorthEast().lng(),
- address.geometry.bounds.getNorthEast().lat()
- ];
- }
- if (bbox.length != 4) {
- return false;
- }
- bbox = new OpenLayers.Bounds(bbox);
- if (extent.intersectsBounds(bbox)) {
- var lab = address.formatted_address.replace(labrex, '$1');
- var wkt = 'POINT(' + address.geometry.location.lng() + ' ' + address.geometry.location.lat() + ')';
- text += '' + lab + '';
- count++;
- }
- }
- if (count == 0 || text == '') {
- text = '' + lizDict['externalsearch.notfound'] + '';
- }
- this._updateExternalSearch('Google');
- } else {
- this._updateExternalSearch('Google- ' + lizDict['externalsearch.notfound'] + '
');
- }
- });
- break;
+ const autoSearch = () => {
+ if ($('#search-query').val().length < AUTOCOMPLETE_MIN_LENGTH) {
+ $('#lizmap-search .items').html('');
+ $('#lizmap-search, #lizmap-search-close').removeClass('open');
+ return;
}
+ this._performExternalSearch(searchConfig, service, extent);
+ };
+
+ $('#nominatim-search').submit(() => {
+ this._performExternalSearch(searchConfig, service, extent);
return false;
});
+ $('#search-query').on('input', this._debounce(autoSearch, 100));
+
return true;
}
diff --git a/lizmap/modules/view/templates/map_headermenu.tpl b/lizmap/modules/view/templates/map_headermenu.tpl
index 558fac7a40..9fba6f7312 100644
--- a/lizmap/modules/view/templates/map_headermenu.tpl
+++ b/lizmap/modules/view/templates/map_headermenu.tpl
@@ -2,7 +2,7 @@