From 1c60544d83a87ede44dfd2febdec7514903f5f59 Mon Sep 17 00:00:00 2001
From: Benoit Lavenier <benoit.lavenier@e-is.pro>
Date: Fri, 20 May 2022 00:48:52 +0200
Subject: [PATCH] [fix] Fix Wot map loading (was exceed 10000 profiles, so
 force load using geo slice)

---
 .../map/js/controllers/wot-controllers.js     |  42 ++--
 www/plugins/map/js/services/utils-services.js |   2 +-
 www/plugins/map/js/services/wot-services.js   | 218 +++++++++---------
 3 files changed, 131 insertions(+), 131 deletions(-)

diff --git a/www/plugins/map/js/controllers/wot-controllers.js b/www/plugins/map/js/controllers/wot-controllers.js
index 6dcf422e3..7e60ae249 100644
--- a/www/plugins/map/js/controllers/wot-controllers.js
+++ b/www/plugins/map/js/controllers/wot-controllers.js
@@ -156,8 +156,10 @@ function MapWotViewController($scope, $filter, $templateCache, $interpolate, $ti
         $scope.map.center.zoom = parseInt(cPart[2]);
       }
 
+      var lastLocationUpdate = Date.now();
       $scope.$watch("map.center", function() {
-        if (!$scope.loading) {
+        if (!$scope.loading && (Date.now() - lastLocationUpdate) > 5000) {
+          lastLocationUpdate = Date.now();
           return $timeout(function() {
             $scope.updateLocationHref();
           }, 300);
@@ -279,22 +281,19 @@ function MapWotViewController($scope, $filter, $templateCache, $interpolate, $ti
         .then($scope.load);
     }
 
+    console.info('[map] Leaflet map is ready: starting loading data...');
     $scope.loading = true;
     // Show loading indicator
     map.fire('dataloading');
 
+    // add bounding box
     var options = {
-      fields: {
-        description: $scope.enableDescription
-      }
+      bounds: angular.copy($scope.map.bounds)
     };
+    delete options.bounds.options;
 
-    // add bounding box
-    if ($scope.map.bounds) {
-      // FIXME - this is not working well
-      //options.bounds = angular.copy($scope.map.bounds);
-      //delete options.bounds.options;
-    }
+    var formatPubkey = $filter('formatPubkey');
+    var markerTemplate = $templateCache.get('plugins/map/templates/wot/popup_marker.html');
 
     // Load wot data, from service
     return mapWot.load(options)
@@ -302,26 +301,24 @@ function MapWotViewController($scope, $filter, $templateCache, $interpolate, $ti
       .then(function(res) {
         var markers = {};
 
-
         if (res && res.length) {
 
-          var formatPubkey = $filter('formatPubkey');
-          var markerTemplate = $templateCache.get('plugins/map/templates/wot/popup_marker.html');
-
-
           _.forEach(res, function (hit) {
             var type = hit.pending ? 'pending' : (hit.uid ? 'member' : 'wallet');
             var id = hit.index + '_' + (hit.id || (hit.uid ? (hit.uid + ':' + hit.pubkey) : hit.pubkey)).replace(/-/g, '_');
             var title = hit.name + ' | ' + formatPubkey(hit.pubkey);
             var marker = {
+              id: id,
+              message: markerTemplate,
+              title: title,
               layer: type,
               icon: icons[type],
               opacity: hit.uid ? 1 : 0.7,
-              title: title,
               lat: hit.geoPoint.lat,
               lng: hit.geoPoint.lon,
+              focus: false,
               getMessageScope: function () {
-                //console.debug('[map] Loading marker ' + title + "...");
+                console.debug('[map] Loading marker ' + title + "...");
                 var markerScope = $scope.$new();
                 markerScope.loadingMarker = true;
                 markerScope.formData = {};
@@ -331,15 +328,12 @@ function MapWotViewController($scope, $filter, $templateCache, $interpolate, $ti
                     uid: hit.uid,
                     name: title,
                     profile: hit,
-                    isMember: type == 'member'			  
+                    isMember: type === 'member'
                   };
                   markerScope.loadingMarker = false;
                 });
                 return markerScope;
-              },
-              focus: false,
-              message: markerTemplate,
-              id: id
+              }
             };
             markers[id] = marker;
           });
@@ -369,10 +363,10 @@ function MapWotViewController($scope, $filter, $templateCache, $interpolate, $ti
   $scope.updateLocationHref = function(centerHash) {
     // removeIf(device)
     var params = $location.search() || {};
-    if (!params.c || !MapUtils.center.isDefault($scope.map.center)) {
+    /*if (!params.c || !MapUtils.center.isDefault($scope.map.center)) {
       centerHash = centerHash || '{0}:{1}:{2}'.format($scope.map.center.lat.toFixed(4), $scope.map.center.lng.toFixed(4), $scope.map.center.zoom);
       $location.search({c: centerHash}).replace();
-    }
+    }*/
     // endRemoveIf(device)
   };
 
diff --git a/www/plugins/map/js/services/utils-services.js b/www/plugins/map/js/services/utils-services.js
index 1d95ce26e..3154eaaba 100644
--- a/www/plugins/map/js/services/utils-services.js
+++ b/www/plugins/map/js/services/utils-services.js
@@ -13,7 +13,7 @@ angular.module('cesium.map.utils.services', ['cesium.services', 'ui-leaflet'])
           lat: 46.5588603, lng: 4.229736328124999, zoom: 6
         }
       },
-      LOCALIZE_ZOOM: 15
+      LOCALIZE_ZOOM: 16
     },
     data = {
       cache: {}
diff --git a/www/plugins/map/js/services/wot-services.js b/www/plugins/map/js/services/wot-services.js
index e34fa9e9c..f8c2b17c3 100644
--- a/www/plugins/map/js/services/wot-services.js
+++ b/www/plugins/map/js/services/wot-services.js
@@ -22,75 +22,92 @@ angular.module('cesium.map.wot.services', ['cesium.services'])
     }
   };
 
+  /**
+   * Convert Leaflet BBox into ES BBox query
+   * see https://www.elastic.co/guide/en/elasticsearch/reference/2.4/geo-point.html
+   * @param bounds
+   */
   function createFilterQuery(options) {
-    options = options || {};
-    var query = {
-      bool: {}
-    };
+    var bounds = options && options.bounds;
+    if (!bounds) throw new Error('Missing options.bounds!');
 
-    // Limit to profile with geo point
-    if (options.searchAddress) {
-      query.bool.should = [
+    var minLon = Math.min(bounds.northEast.lng, bounds.southWest.lng),
+      minLat = Math.min(bounds.northEast.lat, bounds.southWest.lat),
+      maxLon = Math.max(bounds.northEast.lng, bounds.southWest.lng),
+      maxLat = Math.max(bounds.northEast.lat, bounds.southWest.lat);
+
+    return {constant_score: {
+      filter: [
         {exists: {field: "geoPoint"}},
-        {exists: {field: "city"}}
-      ];
-    }
-    else {
-      query.bool.must= [
-        {exists: {field: "geoPoint"}}
-      ];
-    }
+        {geo_bounding_box: {geoPoint: {
+              top_left: {
+                lat: maxLat,
+                lon: minLon
+              },
+              bottom_right: {
+                lat: minLat,
+                lon: maxLon
+              }
+            }
+          }}
+      ]
+    }};
+  }
 
-    // Filter on bounding box
-    // see https://www.elastic.co/guide/en/elasticsearch/reference/2.4/geo-point.html
-    if (options.bounds && options.bounds.northEast && options.bounds.southWest) {
-      var boundingBox = {
-        "geoPoint" : {
-          "top_left" : {
-            "lat" : Math.max(Math.min(options.bounds.northEast.lat, 90), -90),
-            "lon" : Math.max(Math.min(options.bounds.southWest.lng, 180), -180)
-          },
-          "bottom_right" : {
-            "lat" : Math.max(Math.min(options.bounds.southWest.lat, 90), -90),
-            "lon" : Math.max(Math.min(options.bounds.northEast.lng, 180), -180)
-          }
-        }
-      };
-      console.debug("[map] [wot] Filtering on bounds: ", options.bounds);
-      query.bool.must = query.bool.must || [];
-      query.bool.must.push({geo_bounding_box:  boundingBox});
+  function createSliceQueries(options, total) {
+    var bounds = options && options.bounds;
+    if (!bounds) throw new Error('Missing options.bounds!');
+
+    var minLon = Math.min(bounds.northEast.lng, bounds.southWest.lng),
+      minLat = Math.min(bounds.northEast.lat, bounds.southWest.lat),
+      maxLon = Math.max(bounds.northEast.lng, bounds.southWest.lng),
+      maxLat = Math.max(bounds.northEast.lat, bounds.southWest.lat);
+
+    var queries = [];
+    var lonStep = (maxLon - minLon) / 5;
+    var latStep = (maxLat - minLat) / 5;
+    for (var lon = minLon; lon < maxLon; lon += lonStep) {
+      for (var lat = minLat; lat < maxLat; lat += latStep) {
+        queries.push({constant_score: {
+            filter: [
+              {exists: {field: "geoPoint"}},
+              {geo_bounding_box: {
+                geoPoint: {
+                  top_left: {
+                    lat: lat + latStep,
+                    lon: lon
+                  },
+                  bottom_right: {
+                    lat: lat,
+                    lon: lon + lonStep
+                  }
+                }
+              }}
+            ]
+          }});
+      }
     }
-    return query;
+    return queries;
   }
 
   function load(options) {
     options = options || {};
     options.from = options.from || 0;
     options.size = options.size || constants.DEFAULT_LOAD_SIZE;
-    options.searchAddress = esGeo.google.isEnable() && (angular.isDefined(options.searchAddress) ? options.searchAddress : true);
 
     options.fields = options.fields || {};
-    options.fields.description = angular.isDefined(options.fields.description) ? options.fields.description : false;
 
-    var request = {
+    var countRequest = {
       query: createFilterQuery(options),
-      from: 0,
-      size: options.size,
-      _source: options.fields.description ? fields.profile.concat("description") : fields.profile
+      size: 0
     };
 
     var mixedSearch = false;
-    /*var mixedSearch = esSettings.wot.isMixedSearchEnable();
-    if (mixedSearch) {
-      // add special fields for page and group
-      request._source = request._source.concat(["type", "pubkey", "issuer", "category"]);
-      console.debug("[ES] [map] Mixed search: enable");
-    }*/
 
     var search = mixedSearch ? that.raw.profile.mixedSearch : that.raw.profile.search;
 
     return $q.all([
-        search(request),
+        search(countRequest),
         BMA.wot.member.uids(),
         BMA.wot.member.pending()
           .then(function(res) {
@@ -98,14 +115,18 @@ angular.module('cesium.map.wot.services', ['cesium.services'])
           })
       ])
       .then(function(res) {
+        var total = res[0].hits && res[0].hits.total || 0;
         var uids = res[1];
         var memberships = res[2];
-        res = res[0];
-        if (!res.hits || !res.hits.total) return [];
+
+        if (!total) return []; // No data
+
+        var now = Date.now();
+        console.info('[map] [wot] Loading {0} profiles...'.format(total));
 
         // Transform pending MS into a map by pubkey
         memberships = memberships.reduce(function(res, ms){
-          if (ms.membership == 'IN' && !uids[ms.pubkey]) {
+          if (ms.membership === 'IN' && !uids[ms.pubkey]) {
             var idty = {
               uid: ms.uid,
               pubkey: ms.pubkey,
@@ -122,26 +143,47 @@ angular.module('cesium.map.wot.services', ['cesium.services'])
           return res;
         }, {});
 
-        var jobs = [
-          processLoadHits(options, uids, memberships, res)
-        ];
-
-        // Additional slice requests
-        request.from += request.size;
+        var searchRecursive = function(request, result) {
+          request.from = request.from || 0;
+          request.size = request.size || constants.DEFAULT_LOAD_SIZE;
+          result = result || {hits: {hits: []}};
+
+          // DEBUG
+          //console.debug('Searching... ' + request.from);
+
+          return search(request).then(function(res) {
+            if (!res.hits || !res.hits.hits.length) return result;
+            result.hits.total = res.hits.total
+            result.hits.hits = result.hits.hits.concat(res.hits.hits);
+            if (result.hits.hits.length < result.hits.total) {
+              request.from += request.size;
+              if (request.from >= 10000) {
+                console.error("Cannot load more than 10000 profiles in a slice. Please reduce slice size!");
+                return result; // Skip if too large
+              }
+              return searchRecursive(request, result);
+            }
+            return result;
+          })
+        }
         var processRequestResultFn = function(subRes) {
           if (!subRes.hits || !subRes.hits.hits.length) return [];
           return processLoadHits(options, uids, memberships, subRes);
         };
-        while (request.from < res.hits.total) {
-          var searchRequest = search(angular.copy(request)).then(processRequestResultFn);
-          jobs.push(searchRequest);
-          request.from += request.size;
-        }
-        return $q.all(jobs)
+
+
+        return $q.all(createSliceQueries(options, total)
+          .reduce(function(res, query) {
+            var request = {query: query, _source: fields.profile};
+            return res.concat(searchRecursive(request).then(processRequestResultFn));
+          }, []))
           .then(function(res){
-            return res.reduce(function(res, items) {
+            var result = res.reduce(function(res, items) {
               return res.concat(items);
             }, []);
+            console.info('[map] [wot] Loaded {0} profiles in {1}ms'.format(result.length, Date.now() - now));
+
+            return result;
           });
       });
   }
@@ -149,8 +191,7 @@ angular.module('cesium.map.wot.services', ['cesium.services'])
   function processLoadHits(options, uids, memberships, res) {
 
     // Transform profile hits
-    var commaRegexp = new RegExp('[,]');
-    var searchAddressItems = [];
+    var commaRegexp = new RegExp(',');
     var items = res.hits.hits.reduce(function(res, hit) {
       var pubkey =  hit._id;
       var uid = uids[pubkey];
@@ -164,19 +205,12 @@ angular.module('cesium.map.wot.services', ['cesium.services'])
 
       // Set geo point
       item.geoPoint = hit._source.geoPoint;
-      if (!item.geoPoint || !item.geoPoint.lat || !item.geoPoint.lon) {
-        if (!options.searchAddress || !item.city) return res; // no city: exclude this item
-        item.searchAddress = item.city && ((hit._source.address ? hit._source.address+ ', ' : '') + item.city);
-        searchAddressItems.push(item);
+      // Convert lat/lon to float (if need)
+      if (item.geoPoint.lat && typeof item.geoPoint.lat === 'string') {
+        item.geoPoint.lat = parseFloat(item.geoPoint.lat.replace(commaRegexp, '.'));
       }
-      else {
-        // Convert lat/lon to float (if need)
-        if (item.geoPoint.lat && typeof item.geoPoint.lat === 'string') {
-          item.geoPoint.lat = parseFloat(item.geoPoint.lat.replace(commaRegexp, '.'));
-        }
-        if (item.geoPoint.lon && typeof item.geoPoint.lon === 'string') {
-          item.geoPoint.lon = parseFloat(item.geoPoint.lon.replace(commaRegexp, '.'));
-        }
+      if (item.geoPoint.lon && typeof item.geoPoint.lon === 'string') {
+        item.geoPoint.lon = parseFloat(item.geoPoint.lon.replace(commaRegexp, '.'));
       }
 
       // Avatar
@@ -190,39 +224,11 @@ angular.module('cesium.map.wot.services', ['cesium.services'])
       }
 
       // Description
-      item.description = hit._source.description && esHttp.util.parseAsHtml(hit._source.description);
+      //item.description = hit._source.description && esHttp.util.parseAsHtml(hit._source.description);
 
       return item.geoPoint ? res.concat(item) : res;
     }, []);
 
-    // Resolve missing positions by addresses (only if google API enable)
-    if (searchAddressItems.length) {
-      var now = Date.now();
-      console.debug('[map] [wot] Search positions of {0} addresses...'.format(searchAddressItems.length));
-      var counter = 0;
-
-      return $q.all(searchAddressItems.reduce(function(res, item) {
-        return !item.city ? res : res.concat(esGeo.google.searchByAddress(item.searchAddress)
-          .then(function(res) {
-            if (!res || !res.length) return;
-            item.geoPoint = res[0];
-            // If search on city, add a randomized delta to avoid superposition
-            if (item.city == item.searchAddress) {
-              item.geoPoint.lon += Math.random() / 1000;
-              item.geoPoint.lat += Math.random() / 1000;
-            }
-            delete item.searchAddress; // not need anymore
-            items.push(item);
-            counter++;
-          })
-          .catch(function() {/*silent*/}));
-      }, []))
-        .then(function(){
-          console.debug('[map] [wot] Resolved {0}/{1} addresses in {2}ms'.format(counter, searchAddressItems.length, Date.now()-now));
-          return items;
-        });
-    }
-
     return $q.when(items);
   }
 
-- 
GitLab