Leaflet – How to Search Through Multiple Columns in QGIS Attribute Table

leafletqgis2web

I have created a map using Leaflet and QGIS2Web. The search engine works fine, however it looks through only one of column of the attribute table. My goal is to have it search through four different columns (Country, division, subdivision, language) rather than just one (language).

For that, I want to create a custom data array from layer features when loading the layer, which will contain four array items for each feature. The new array will then be used as the source data (using sourceData option) for the search engine.

The only issue remaining is that if the user searches for a language that is spoken at various locations in Africa, how can I have the map show all the locations that speak the language searched for?

map.addControl(new L.Control.Search({
        layer: layer_Eth_Region_2013_Project_Merg_1,
        initial: false,
        hideMarkerOnCollapse: true,
        propertyName: 'q2wHide_lang2'}));
    document.getElementsByClassName('search-button')[0].className +=
     ' fa fa-binoculars';


var popupContent = '<table>\
                <tr>\
                    <th scope="row">Language</th>\
                    <td>' + (!!feature.properties['q2wHide_lang2'] ? autolinker.link(feature.properties['q2wHide_lang2'].toLocaleString()) : '') + '</td>\
                </tr>\
                <tr>\
                    <th scope="row">State</th>\
                    <td>' + (!!feature.properties['State'] ? autolinker.link(feature.properties['State'].toLocaleString()) : '') + '</td>\
                </tr>\
                <tr>\
                    <th scope="row">Local Gove</th>\
                    <td>' + (!!feature.properties['Local Gove'] ? autolinker.link(feature.properties['Local Gove'].toLocaleString()) : '') + '</td>\
                </tr>\
                <tr>\
                    <th scope="row">Country</th>\
                    <td>' + (!!feature.properties['Country'] ? autolinker.link(feature.properties['Country'].toLocaleString()) : '') + '</td>\
                </tr>\
            </table>';

Code sample that I will be using to create a custom data array:

var data = [
    {"loc":[41.575330,13.102411], "title":"aquamarine"},
    {"loc":[41.575730,13.002411], "title":"black"},
    {"loc":[41.807149,13.162994], "title":"blue"},
    {"loc":[41.507149,13.172994], "title":"chocolate"},
    {"loc":[41.847149,14.132994], "title":"coral"},
    {"loc":[41.219190,13.062145], "title":"cyan"},
    {"loc":[41.344190,13.242145], "title":"darkblue"},  
    {"loc":[41.679190,13.122145], "title":"darkred"},
    {"loc":[41.329190,13.192145], "title":"darkgray"},
    {"loc":[41.379290,13.122545], "title":"dodgerblue"},
    {"loc":[41.409190,13.362145], "title":"gray"},
    {"loc":[41.794008,12.583884], "title":"green"}, 
    {"loc":[41.805008,12.982884], "title":"greenyellow"},
    {"loc":[41.536175,13.273590], "title":"red"},
    {"loc":[41.516175,13.373590], "title":"rosybrown"},
    {"loc":[41.506175,13.173590], "title":"royalblue"},
    {"loc":[41.836175,13.673590], "title":"salmon"},
    {"loc":[41.796175,13.570590], "title":"seagreen"},
    {"loc":[41.436175,13.573590], "title":"seashell"},
    {"loc":[41.336175,13.973590], "title":"silver"},
    {"loc":[41.236175,13.273590], "title":"skyblue"},
    {"loc":[41.546175,13.473590], "title":"yellow"},
    {"loc":[41.239190,13.032145], "title":"white"}
];

var map = new L.Map('map', {zoom: 9, center: new L.latLng(data[0].loc) });

map.addLayer(new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'));    //base layer

function localData(text, callResponse)
{
    //here can use custom criteria or merge data from multiple layers

    callResponse(data);

    return {    //called to stop previous requests on map move
        abort: function() {
            console.log('aborted request:'+ text);
        }
    };
}

Best Answer

Answer will be based on sourceData demo example code.

Search control sourceData option is a function that has to prepare an array of objects to be searched (here it's static array data), where each object has loc property with location coordinate and title property with associated search text. There is only one coordinate allowed per search text, so if you want to have several of them, a bit of trickery is needed.

Instead of loc property holding real world coordinates, it can in one of the coordinates just hold an index that points to an array (here it's dataCoords), where each element is an array of coordinates, corresponding to the index (if this sounds too complicated, see code below).

Since returned location coordinate now won't be real coordinate but just pointer to array of coordinates, search control moveToLocation option has to be used to process the result of search, which means creating markers in zooming to locations found.

Standard markers display in this case has to be disabled.

Code could then look something like this:

var dataCoords = [
 [[41.575330,13.102411], [41.575730,13.002411]],
 [[41.807149,13.162994], [41.507149,13.172994], [41.847149,14.132994]],
 [[41.219190,13.062145]],
 [[41.344190,13.242145], [41.679190,13.122145]],
 [[41.329190,13.192145], [41.379290,13.122545], [41.409190,13.362145], [41.794008,12.583884]]
];

var data = [
  {"loc":[0,0], "title":"aquamarine"},
  {"loc":[1,0], "title":"black"},
  {"loc":[2,0], "title":"blue"},
  {"loc":[3,0], "title":"chocolate"},
  {"loc":[4,0], "title":"coral"}
];


var map = new L.Map('map', {zoom: 9, center: new L.latLng([41.575330,13.102411]) });
map.addLayer(new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'));

function localData(text, callResponse)
{
  callResponse(data);
  return {
    abort: function() {}
  };
}

var markerLayer = L.layerGroup().addTo(map);

var searchControl = new L.Control.Search({
  sourceData: localData,
  moveToLocation: function (latlng, title, map) {
    markerLayer.clearLayers();
    locations = dataCoords[latlng.lat];
    locations.forEach(function(locationCoords) {
      L.circleMarker(locationCoords).addTo(markerLayer);
    });
    map.fitBounds(L.latLngBounds(locations).pad(0.1));
  },
  text:'Color...',
  markerLocation: false,
  marker: false
});
map.addControl(searchControl);
Related Question