From 59e9f3ad28aa1a74fc27b6913a7e284b60863014 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:05:16 +0200 Subject: [PATCH 01/31] create marker source and layers --- .../static/geospatial/js/case_management.js | 9 --- .../geospatial/static/geospatial/js/models.js | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/case_management.js b/corehq/apps/geospatial/static/geospatial/js/case_management.js index fdef307a1a5c..1098ae56b6f0 100644 --- a/corehq/apps/geospatial/static/geospatial/js/case_management.js +++ b/corehq/apps/geospatial/static/geospatial/js/case_management.js @@ -16,15 +16,6 @@ hqDefine("geospatial/js/case_management", [ utils, alertUser ) { - const caseMarkerColors = { - 'default': "#808080", // Gray - 'selected': "#00FF00", // Green - }; - const userMarkerColors = { - 'default': "#0e00ff", // Blue - 'selected': "#0b940d", // Dark Green - }; - const MAP_CONTAINER_ID = 'geospatial-map'; const SHOW_USERS_QUERY_PARAM = 'show_users'; const USER_LOCATION_ID_QUERY_PARAM = 'user_location_id'; diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index 7139d8d166e8..5c58a47ea71c 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -23,6 +23,14 @@ hqDefine('geospatial/js/models', [ "Oops! Something went wrong!" + " Please report an issue if the problem persists." ); + const caseMarkerColors = { + 'default': "#808080", // Gray + 'selected': "#00FF00", // Green + }; + const userMarkerColors = { + 'default': "#0e00ff", // Blue + 'selected': "#0b940d", // Dark Green + }; var MissingGPSModel = function () { this.casesWithoutGPS = ko.observable([]); @@ -120,6 +128,14 @@ hqDefine('geospatial/js/models', [ var Map = function (usesClusters, usesStreetsLayers) { var self = this; + self.userLayerName = 'user-points'; + self.caseLayerName = 'case-points'; + self.dataPointsSourceName = 'data-points'; + self.sourceData = { + 'type': 'FeatureCollection', + 'features': [], + }; + self.usesClusters = usesClusters; self.usesStreetsLayers = usesStreetsLayers; @@ -164,6 +180,8 @@ hqDefine('geospatial/js/models', [ if (self.usesClusters) { createClusterLayers(); + } else { + self.mapInstance.on('load', createMarkerLayers); } if (self.usesStreetsLayers) { @@ -172,6 +190,56 @@ hqDefine('geospatial/js/models', [ } }; + function createMarkerLayers() { + self.mapInstance.addSource(self.dataPointsSourceName, { + 'type': 'geojson', + 'data': self.sourceData, + }); + + self.mapInstance.loadImage( + 'https://docs.mapbox.com/mapbox-gl-js/assets/custom_marker.png', + (error, image) => { + if (error) { + throw error; + } + self.mapInstance.addImage('custom-marker', image, {sdf: true}); + self.createMarkerLayer('case-points', self.dataPointsSourceName, 'case', caseMarkerColors.default, caseMarkerColors.selected); + self.createMarkerLayer('user-points', self.dataPointsSourceName, 'user', userMarkerColors.default, userMarkerColors.selected); + } + ); + } + + self.createMarkerLayer = function (layerName, source, itemType, defaultColor, selectedColor) { + self.mapInstance.addLayer({ + 'id': layerName, + 'type': 'symbol', + 'source': source, + 'layout': { + 'icon-image': 'custom-marker', + 'icon-size': [ + 'interpolate', + ['exponential', 2], + ['zoom'], + 5, 0.35, + 15, 3, + ], + 'icon-allow-overlap': true, + }, + 'paint': { + 'icon-color': [ + 'match', + ['get', 'selected'], + 'false', + defaultColor, + 'true', + selectedColor, + '#0000FF', + ], + }, + 'filter': ['==', ['get', 'type'], itemType], + }); + }; + function loadMapBoxStreetsLayers() { self.mapInstance.on('load', () => { self.mapInstance.addSource('mapbox-streets', { From a3674703922fa7277ab3f65ff346b1e45c5de10a Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:05:33 +0200 Subject: [PATCH 02/31] helper functions to manage source --- .../geospatial/static/geospatial/js/models.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index 5c58a47ea71c..c314170fab57 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -190,6 +190,34 @@ hqDefine('geospatial/js/models', [ } }; + self.addDataToSource = function (features) { + self.sourceData.features = self.sourceData.features.concat(features); + self.refreshSource(); + }; + + self.removeItemTypeFromSource = function (itemType) { + self.sourceData.features = self.sourceData.features.filter( + feature => feature.properties.type !== itemType + ); + self.refreshSource(); + }; + + self.refreshSource = function () { + let source = self.mapInstance.getSource(self.dataPointsSourceName); + if (!source) { + return; + } + source.setData(self.sourceData); + }; + + self.updatePropertyInSource = function (itemId, propName, propVal) { + const featureIndex = self.sourceData.features.findIndex(feature => feature.properties.id === itemId); + if (featureIndex >= 0) { + self.sourceData.features[featureIndex].properties[propName] = propVal; + self.refreshSource(); + } + }; + function createMarkerLayers() { self.mapInstance.addSource(self.dataPointsSourceName, { 'type': 'geojson', From 1f523a3c7c666b9d3bfa2c059a1fe53968a69a76 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:07:26 +0200 Subject: [PATCH 03/31] refactor mapitem model to work with layer --- .../geospatial/static/geospatial/js/models.js | 70 ++++++++++--------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index c314170fab57..f186689fe959 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -44,42 +44,31 @@ hqDefine('geospatial/js/models', [ self.geoJson = polygon.geo_json; }; - var MapItem = function (itemId, itemData, marker, markerColors) { - var self = this; - self.itemId = itemId; - self.itemData = itemData; - self.marker = marker; - self.selectCssId = "select" + itemId; - self.isSelected = ko.observable(false); - self.markerColors = markerColors; - - self.groupId = null; - self.groupCoordinates = null; - - self.setMarkerOpacity = function (opacity) { - const element = self.marker.getElement(); - const svg = element.getElementsByTagName("svg")[0]; - svg.setAttribute("opacity", opacity); - }; - - function changeMarkerColor(selectedCase, newColor) { - let marker = selectedCase.marker; - let element = marker.getElement(); - let svg = element.getElementsByTagName("svg")[0]; - let path = svg.getElementsByTagName("path")[0]; - path.setAttribute("fill", newColor); - } + var MapItem = function (data, mapModel) { + let self = this; + self.itemId = data.id; + self.name = data.name; + self.coordinates = data.coordinates; + self.itemType = data.itemType; + self.link = data.link; + self.selectCssId = `select_${self.itemId}`; + self.isSelected = ko.observable(data.isSelected); + self.mapModel = mapModel; + self.customData = data.customData; self.getItemType = function () { - if (self.itemData.type === "user") { + if (self.itemType === 'user') { return gettext("Mobile Worker"); } return gettext("Case"); }; + function changeMarkerColor(val) { + self.mapModel.updatePropertyInSource(self.itemId, 'selected', val); + self.mapModel.refreshSource(); + } + self.updateCheckbox = function () { - // Need to update the checkbox through JQuery as we can't rely on dynamically changing its value - // with an observable. Doing so breaks all KO bindings in the element const checkbox = $(`#${self.selectCssId}`); if (!checkbox) { return; @@ -88,16 +77,33 @@ hqDefine('geospatial/js/models', [ }; self.isSelected.subscribe(function () { - // Popup might be open when value changes, so make sure checkbox shows correct value self.updateCheckbox(); - var color = self.isSelected() ? self.markerColors.selected : self.markerColors.default; - changeMarkerColor(self, color); + const isSelected = self.isSelected().toString(); + changeMarkerColor(isSelected); }); self.getJson = function () { return { 'id': self.itemId, - 'text': self.itemData.name, + 'text': self.name, + }; + }; + + self.getGeoJson = function () { + return { + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': [ + self.coordinates.lng, + self.coordinates.lat, + ], + }, + 'properties': { + 'selected': self.isSelected().toString(), // Can only use numbers and strings, so use a str to represent true/false + 'id': self.itemId, + 'type': self.itemType, + }, }; }; }; From b8579aca96f3005767afd9f723405075e50c8c12 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:09:57 +0200 Subject: [PATCH 04/31] create popup when clicking on marker --- .../geospatial/static/geospatial/js/models.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index f186689fe959..be5a92bd63e4 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -238,7 +238,13 @@ hqDefine('geospatial/js/models', [ } self.mapInstance.addImage('custom-marker', image, {sdf: true}); self.createMarkerLayer('case-points', self.dataPointsSourceName, 'case', caseMarkerColors.default, caseMarkerColors.selected); + self.mapInstance.on('click', 'case-points', (e) => { + markerClickEvent(e, 'case'); + }); self.createMarkerLayer('user-points', self.dataPointsSourceName, 'user', userMarkerColors.default, userMarkerColors.selected); + self.mapInstance.on('click', 'user-points', (e) => { + markerClickEvent(e, 'user'); + }); } ); } @@ -274,6 +280,31 @@ hqDefine('geospatial/js/models', [ }); }; + function markerClickEvent(e, itemType) { + const coordinates = e.features[0].geometry.coordinates.slice(); + const markerId = e.features[0].properties.id; + + // Ensure that if the map is zoomed out such that multiple + // copies of the feature are visible, the popup appears + // over the copy being pointed to. + if (['mercator', 'equirectangular'].includes(self.mapInstance.getProjection().name)) { + while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) { + coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360; + } + } + + const popupDiv = document.createElement('div'); + const markerItems = (itemType === 'user') ? self.userMapItems() : self.caseMapItems(); + const mapItem = markerItems.find((mapItem) => { + return markerId === mapItem.itemId; + }); + + const openFunc = () => mapItem.updateCheckbox(); + const popup = utils.createMapPopup(coordinates, popupDiv, openFunc); + popup.addTo(self.mapInstance); + $(popupDiv).koApplyBindings(mapItem); + } + function loadMapBoxStreetsLayers() { self.mapInstance.on('load', () => { self.mapInstance.addSource('mapbox-streets', { From a2d34d1ebbf96988b16b9849bbeaf94b3b8abc63 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:13:47 +0200 Subject: [PATCH 05/31] refactor helper functions to select map items --- .../geospatial/static/geospatial/js/models.js | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index be5a92bd63e4..87a19b569c3d 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -586,31 +586,30 @@ hqDefine('geospatial/js/models', [ }; self.selectMapItemInPolygons = function (polygonArr, mapItem) { - let isSelected = false; - for (const polygon of polygonArr) { + const isSelected = self.isMapItemInPolygons(polygonArr, mapItem.coordinates); + mapItem.isSelected(isSelected); + }; + + self.isMapItemInPolygons = function (polygonFeatures, coordinates) { + for (const polygon of polygonFeatures) { if (polygon.geometry.type !== 'Polygon') { continue; } - if (isMapItemInPolygon(polygon, mapItem.itemData.coordinates)) { - isSelected = true; - break; + // Will be 0 if a user deletes a point from a three-point polygon, + // since mapbox will delete the entire polygon. turf.booleanPointInPolygon() + // does not expect this, and will raise a 'TypeError' exception. + if (!polygon.geometry.coordinates.length) { + continue; + } + const coordinatesArr = [coordinates.lng, coordinates.lat]; + const point = turf.point(coordinatesArr); // eslint-disable-line no-undef + if (turf.booleanPointInPolygon(point, polygon.geometry)) { + return true; } } - mapItem.isSelected(isSelected); + return false; }; - function isMapItemInPolygon(polygonFeature, coordinates) { - // Will be 0 if a user deletes a point from a three-point polygon, - // since mapbox will delete the entire polygon. turf.booleanPointInPolygon() - // does not expect this, and will raise a 'TypeError' exception. - if (!polygonFeature.geometry.coordinates.length) { - return false; - } - const coordinatesArr = [coordinates.lng, coordinates.lat]; - const point = turf.point(coordinatesArr); // eslint-disable-line no-undef - return turf.booleanPointInPolygon(point, polygonFeature.geometry); // eslint-disable-line no-undef - } - self.mapHasPolygons = function () { const drawnFeatures = self.drawControls.getAll().features; if (!drawnFeatures.length) { From 3ab38698a99bc7ceba567e613e03202106cf1126 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:14:38 +0200 Subject: [PATCH 06/31] refactor loading cases to source --- .../static/geospatial/js/case_management.js | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/case_management.js b/corehq/apps/geospatial/static/geospatial/js/case_management.js index 1098ae56b6f0..86f5a58ad8aa 100644 --- a/corehq/apps/geospatial/static/geospatial/js/case_management.js +++ b/corehq/apps/geospatial/static/geospatial/js/case_management.js @@ -224,14 +224,17 @@ hqDefine("geospatial/js/case_management", [ }); } - function selectMapItemsInPolygons() { + function getMapPolygons() { let features = mapModel.drawControls.getAll().features; if (polygonFilterModel.activeSavedPolygon()) { features = features.concat(polygonFilterModel.activeSavedPolygon().geoJson.features); } - if (features.length) { - mapModel.selectAllMapItems(features); - } + return features; + } + + function selectMapItemsInPolygons() { + const polygons = getMapPolygons(); + mapModel.selectAllMapItems(polygons); } function initPolygonFilters() { @@ -428,19 +431,28 @@ hqDefine("geospatial/js/case_management", [ } function loadCases(caseData) { - mapModel.removeMarkersFromMap(mapModel.caseMapItems()); + mapModel.removeItemTypeFromSource('case'); mapModel.caseMapItems([]); - var casesWithGPS = caseData.filter(function (item) { - return item[1] !== null; - }); - // Index by case_id - var casesById = _.object(_.map(casesWithGPS, function (item) { - if (item[1]) { - return [item[0], {'coordinates': item[1], 'link': item[2], 'type': 'case', 'name': item[3]}]; - } - })); - const caseMapItems = mapModel.addMarkersToMap(casesById, caseMarkerColors); + let features = []; + let caseMapItems = []; + const polygonFeatures = getMapPolygons(); + for (const caseItem of caseData) { + const isInPolygon = mapModel.isMapItemInPolygons(polygonFeatures, caseItem[1]); + const parsedData = { + id: caseItem[0], + coordinates: caseItem[1], + link: caseItem[2], + name: caseItem[3], + itemType: 'case', + isSelected: isInPolygon, + customData: {}, + }; + const caseMapItem = new models.MapItem(parsedData, mapModel); + caseMapItems.push(caseMapItem); + features.push(caseMapItem.getGeoJson()); + } mapModel.caseMapItems(caseMapItems); + mapModel.addDataToSource(features); mapModel.fitMapBounds(caseMapItems); } @@ -494,13 +506,12 @@ hqDefine("geospatial/js/case_management", [ ); } } else if (xhr.responseJSON.aaData.length && mapModel.mapInstance) { - loadCases(xhr.responseJSON.aaData); - if (polygonFilterModel) { - selectMapItemsInPolygons(); - } - if (mapModel.hasDisbursementLayer()) { - mapModel.removeDisbursementLayer(); - } + mapModel.mapInstance.on('load', () => { + loadCases(xhr.responseJSON.aaData); + if (mapModel.hasDisbursementLayer()) { + mapModel.removeDisbursementLayer(); + } + }); } }); }); From cc900533b78b8c8e86e1309acc2de4f04824c0e3 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:15:26 +0200 Subject: [PATCH 07/31] refactor loading users to source --- .../static/geospatial/js/case_management.js | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/case_management.js b/corehq/apps/geospatial/static/geospatial/js/case_management.js index 86f5a58ad8aa..af2de2fa33ac 100644 --- a/corehq/apps/geospatial/static/geospatial/js/case_management.js +++ b/corehq/apps/geospatial/static/geospatial/js/case_management.js @@ -307,7 +307,7 @@ hqDefine("geospatial/js/case_management", [ }; self.loadUsers = function () { - mapModel.removeMarkersFromMap(mapModel.userMapItems()); + mapModel.removeItemTypeFromSource('user'); mapModel.userMapItems([]); self.hasErrors(false); if (!self.shouldShowUsers()) { @@ -321,30 +321,39 @@ hqDefine("geospatial/js/case_management", [ url: initialPageData.reverse('get_users_with_gps'), success: function (data) { self.hasFiltersChanged(false); - const userData = _.object(_.map(data.user_data, function (userData) { + let features = []; + let userMapItems = []; + const polygonFeatures = getMapPolygons(); + for (const userData of data.user_data) { const gpsData = (userData.gps_point) ? userData.gps_point.split(' ') : []; - const lat = parseFloat(gpsData[0]); - const lng = parseFloat(gpsData[1]); - + if (!gpsData.length) { + continue; + } + const coordinates = { + 'lat': gpsData[0], + 'lng': gpsData[1], + }; const editUrl = initialPageData.reverse('edit_commcare_user', userData.id); const link = `${userData.username}`; - - const userInfo = { - 'coordinates': { - 'lat': lat, - 'lng': lng, + const isInPolygon = mapModel.isMapItemInPolygons(polygonFeatures, coordinates); + const parsedData = { + id: userData.id, + coordinates: coordinates, + link: link, + name: userData.username, + itemType: 'user', + isSelected: isInPolygon, + customData: { + primary_loc_name: userData.primary_loc_name, }, - 'link': link, - 'type': 'user', - 'name': userData.username, - 'primary_loc_name': userData.primary_loc_name, }; - return [userData.id, userInfo]; - })); - const userMapItems = mapModel.addMarkersToMap(userData, userMarkerColors); + const userMapItem = new models.MapItem(parsedData, mapModel); + userMapItems.push(userMapItem); + features.push(userMapItem.getGeoJson()); + } + mapModel.addDataToSource(features); mapModel.userMapItems(userMapItems); - selectMapItemsInPolygons(); }, error: function () { self.hasErrors(true); From 7c8482b3ceaa205c0528f31055fb108611079003 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:16:45 +0200 Subject: [PATCH 08/31] remove redundant helper functions --- .../geospatial/static/geospatial/js/models.js | 89 ------------------- 1 file changed, 89 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index 87a19b569c3d..0fa060c34514 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -482,95 +482,6 @@ hqDefine('geospatial/js/models', [ }); } - self.removeMarkersFromMap = function (itemArr) { - _.each(itemArr, function (currItem) { - currItem.marker.remove(); - }); - }; - - self.addMarkersToMap = function (itemArr, markerColours) { - let outArr = []; - _.forEach(itemArr, function (item, itemId) { - const coordinates = item.coordinates; - if (coordinates && coordinates.lat && coordinates.lng) { - const mapItem = addMarker(itemId, item, markerColours); - outArr.push(mapItem); - } - }); - return outArr; - }; - - function addMarker(itemId, itemData, colors) { - const coordinates = itemData.coordinates; - // Create the marker - const marker = new mapboxgl.Marker({ color: colors.default, draggable: false }); // eslint-disable-line no-undef - marker.setLngLat(coordinates); - - // Add the marker to the map - marker.addTo(self.mapInstance); - - const popupDiv = document.createElement("div"); - - const mapItemInstance = new MapItem(itemId, itemData, marker, colors); - let openFunc; - if (self.usesClusters) { - openFunc = () => highlightMarkerGroup(itemId); - } else { - openFunc = () => mapItemInstance.updateCheckbox(); - } - const popup = utils.createMapPopup( - coordinates, - popupDiv, - openFunc, - resetMarkersOpacity - ); - - marker.setPopup(popup); - $(popupDiv).koApplyBindings(mapItemInstance); - - return mapItemInstance; - } - - function resetMarkersOpacity() { - let markers = []; - Object.keys(self.caseGroupsIndex).forEach(itemCoordinates => { - const mapMarkerItem = self.caseGroupsIndex[itemCoordinates]; - markers.push(mapMarkerItem.item); - - const lineId = self.getLineFeatureId(mapMarkerItem.item.itemId); - if (self.mapInstance.getLayer(lineId)) { - self.mapInstance.setPaintProperty(lineId, 'line-opacity', 1); - } - }); - changeMarkersOpacity(markers, 1); - } - - function highlightMarkerGroup(itemId) { - const markerItem = self.caseGroupsIndex[itemId]; - if (markerItem) { - const groupId = markerItem.groupId; - let markersToHide = []; - Object.keys(self.caseGroupsIndex).forEach(itemCoordinates => { - const mapMarkerItem = self.caseGroupsIndex[itemCoordinates]; - - if (mapMarkerItem.groupId !== groupId) { - markersToHide.push(mapMarkerItem.item); - const lineId = self.getLineFeatureId(mapMarkerItem.item.itemId); - if (self.mapInstance.getLayer(lineId)) { - self.mapInstance.setPaintProperty(lineId, 'line-opacity', DOWNPLAY_OPACITY); - } - } - }); - changeMarkersOpacity(markersToHide, DOWNPLAY_OPACITY); - } - } - - function changeMarkersOpacity(markers, opacity) { - markers.forEach(marker => { - marker.setMarkerOpacity(opacity); - }); - } - self.getLineFeatureId = function (itemId) { return DISBURSEMENT_LAYER_PREFIX + itemId; }; From 4499c7b9d20ed7525af73bb6880b275558e4cd73 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:17:51 +0200 Subject: [PATCH 09/31] refactor assignment class to work with new map item model --- .../apps/geospatial/static/geospatial/js/models.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index 0fa060c34514..8449140cfa1a 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -1004,17 +1004,17 @@ hqDefine('geospatial/js/models', [ const groupData = self.mapModel.caseGroupsIndex; self.caseData = []; for (const item of self.mapModel.caseMapItems()) { - const assignedUserId = groupData[item.itemId].assignedUserId; + const assignedUserId = (groupData[item.itemId]) ? groupData[item.itemId].assignedUserId : null; let assignedUsername = emptyColStr; let primaryLocName = emptyColStr; if (assignedUserId) { - const userData = groupData[assignedUserId].item.itemData; + const userData = groupData[assignedUserId].item; assignedUsername = userData.name; - primaryLocName = userData.primary_loc_name; + primaryLocName = userData.customData.primary_loc_name; } self.caseData.push( new AssignmentRow( - item.itemData.name, item.itemId, assignedUserId, assignedUsername, primaryLocName, item + item.name, item.itemId, assignedUserId, assignedUsername, primaryLocName, item ) ); } @@ -1043,17 +1043,17 @@ hqDefine('geospatial/js/models', [ }; self.assignUserToCases = function () { - const selectedUser = self.mapModel.caseGroupsIndex[self.selectedUserId()]; + const selectedUser = self.mapModel.caseGroupsIndex[self.selectedUserId()].item; for (const caseItem of self.caseDataPage()) { if (!caseItem.isSelected()) { continue; } caseItem.assignedUsername( - (selectedUser) ? selectedUser.item.itemData.name : emptyColStr + (selectedUser) ? selectedUser.name : emptyColStr ); caseItem.assignedUserPrimaryLocName( - (selectedUser) ? selectedUser.item.itemData.primary_loc_name : emptyColStr + (selectedUser) ? selectedUser.customData.primary_loc_name : emptyColStr ); caseItem.assignedUserId = self.selectedUserId(); caseItem.isSelected(false); From f4a642ab9a54ec4570c85ec205bfb37e32365588 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:18:51 +0200 Subject: [PATCH 10/31] remove cases that are not assigned from case group index --- .../apps/geospatial/static/geospatial/js/models.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index 8449140cfa1a..1a1d7de0c457 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -1063,9 +1063,15 @@ hqDefine('geospatial/js/models', [ self.finishAssignment = function () { for (const caseItem of self.caseData) { const userItem = self.mapModel.caseGroupsIndex[caseItem.assignedUserId]; - const groupId = (userItem) ? userItem.groupId : null; - self.mapModel.caseGroupsIndex[caseItem.caseId].assignedUserId = caseItem.assignedUserId; - self.mapModel.caseGroupsIndex[caseItem.caseId].groupId = groupId; + if (userItem) { + self.mapModel.caseGroupsIndex[caseItem.caseId] = { + assignedUserId: caseItem.assignedUserId, + groupId: userItem.groupId, + item: caseItem.mapItem, + }; + } else if (self.mapModel.caseGroupsIndex[caseItem.caseId]) { + delete self.mapModel.caseGroupsIndex[caseItem.caseId]; + } } self.mapModel.removeDisbursementLayer(); @@ -1098,7 +1104,7 @@ hqDefine('geospatial/js/models', [ let caseIdToOwnerId = {}; for (const caseItem of self.mapModel.caseMapItems()) { const caseData = self.mapModel.caseGroupsIndex[caseItem.itemId]; - if (caseData.assignedUserId) { + if (caseData && caseData.assignedUserId) { caseIdToOwnerId[caseData.item.itemId] = caseData.assignedUserId; } } From 202dcb9cb1e9f6dc01b4e73c9bce771379ce9938 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:20:07 +0200 Subject: [PATCH 11/31] create entry for each user in case group index --- .../static/geospatial/js/case_management.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/case_management.js b/corehq/apps/geospatial/static/geospatial/js/case_management.js index af2de2fa33ac..58bb1f557922 100644 --- a/corehq/apps/geospatial/static/geospatial/js/case_management.js +++ b/corehq/apps/geospatial/static/geospatial/js/case_management.js @@ -60,21 +60,23 @@ hqDefine("geospatial/js/case_management", [ let groupId = 0; mapModel.caseGroupsIndex = {}; - Object.keys(result).forEach((userId) => { - const user = mapModel.userMapItems().find((userModel) => {return userModel.itemId === userId;}); - mapModel.caseGroupsIndex[userId] = {groupId: groupId, item: user}; - + for (const userItem of mapModel.userMapItems()) { + mapModel.caseGroupsIndex[userItem.itemId] = {groupId: groupId, item: userItem}; + if (!result[userItem.itemId]) { + groupId++; + continue; + } mapModel.caseMapItems().forEach((caseModel) => { - if (result[userId].includes(caseModel.itemId)) { + if (result[userItem.itemId].includes(caseModel.itemId)) { mapModel.caseGroupsIndex[caseModel.itemId] = { groupId: groupId, item: caseModel, - assignedUserId: userId, + assignedUserId: userItem.itemId, }; } }); groupId += 1; - }); + } self.connectUserWithCasesOnMap(); self.setBusy(false); }; From 665fcf134549ab6b78e6b033ead3ee4c1daf547d Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:21:10 +0200 Subject: [PATCH 12/31] correctly retrieve coordinates from map item --- .../static/geospatial/js/case_grouping_map.js | 2 +- .../static/geospatial/js/case_management.js | 12 ++++++------ .../apps/geospatial/static/geospatial/js/models.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/case_grouping_map.js b/corehq/apps/geospatial/static/geospatial/js/case_grouping_map.js index 0ab85a9565bb..670d1a71eb7f 100644 --- a/corehq/apps/geospatial/static/geospatial/js/case_grouping_map.js +++ b/corehq/apps/geospatial/static/geospatial/js/case_grouping_map.js @@ -184,7 +184,7 @@ hqDefine("geospatial/js/case_grouping_map",[ }; _.each(caseList, function (caseWithGPS) { - const coordinates = caseWithGPS.itemData.coordinates; + const coordinates = caseWithGPS.coordinates; if (coordinates && coordinates.lat && coordinates.lng) { caseLocationsGeoJson["features"].push( { diff --git a/corehq/apps/geospatial/static/geospatial/js/case_management.js b/corehq/apps/geospatial/static/geospatial/js/case_management.js index 58bb1f557922..b36198f0e074 100644 --- a/corehq/apps/geospatial/static/geospatial/js/case_management.js +++ b/corehq/apps/geospatial/static/geospatial/js/case_management.js @@ -89,8 +89,8 @@ hqDefine("geospatial/js/case_management", [ if (!hasSelectedCases || c.isSelected()) { caseData.push({ id: c.itemId, - lon: c.itemData.coordinates.lng, - lat: c.itemData.coordinates.lat, + lon: c.coordinates.lng, + lat: c.coordinates.lat, }); } }); @@ -131,8 +131,8 @@ hqDefine("geospatial/js/case_management", [ if (!hasSelectedUsers || userMapItem.isSelected()) { userData.push({ id: userMapItem.itemId, - lon: userMapItem.itemData.coordinates.lng, - lat: userMapItem.itemData.coordinates.lat, + lon: userMapItem.coordinates.lng, + lat: userMapItem.coordinates.lat, }); } }); @@ -179,8 +179,8 @@ hqDefine("geospatial/js/case_management", [ if ('assignedUserId' in element) { let user = mapModel.caseGroupsIndex[element.assignedUserId].item; const lineCoordinates = [ - [user.itemData.coordinates.lng, user.itemData.coordinates.lat], - [element.item.itemData.coordinates.lng, element.item.itemData.coordinates.lat], + [user.coordinates.lng, user.coordinates.lat], + [element.item.coordinates.lng, element.item.coordinates.lat], ]; disbursementLinesSource.features.push( { diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index 1a1d7de0c457..0d7b08b02fc4 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -543,9 +543,9 @@ hqDefine('geospatial/js/models', [ } // See https://stackoverflow.com/questions/62939325/scale-mapbox-gl-map-to-fit-set-of-markers - const firstCoord = mapItems[0].itemData.coordinates; + const firstCoord = mapItems[0].coordinates; const bounds = mapItems.reduce(function (bounds, mapItem) { - const coord = mapItem.itemData.coordinates; + const coord = mapItem.coordinates; if (coord) { return bounds.extend(coord); } From a56340af2c2823530fa7cc9efaa2000472d75b17 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:22:39 +0200 Subject: [PATCH 13/31] ensure active poly layer does not clash with streets layers --- .../apps/geospatial/static/geospatial/js/models.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index 0d7b08b02fc4..a7643f65bb32 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -701,20 +701,24 @@ hqDefine('geospatial/js/models', [ function removeActivePolygonLayer() { if (self.activeSavedPolygon()) { - self.mapObj.mapInstance.removeLayer(self.activeSavedPolygon().id); - self.mapObj.mapInstance.removeSource(self.activeSavedPolygon().id); + const layerName = self.activeSavedPolygon().id + '-layer'; + const sourceName = self.activeSavedPolygon().id + '-source'; + self.mapObj.mapInstance.removeLayer(layerName); + self.mapObj.mapInstance.removeSource(sourceName); } } function createActivePolygonLayer(polygonObj) { + const layerName = String(polygonObj.id) + '-layer'; + const sourceName = String(polygonObj.id) + '-source'; self.mapObj.mapInstance.addSource( - String(polygonObj.id), + sourceName, {'type': 'geojson', 'data': polygonObj.geoJson} ); self.mapObj.mapInstance.addLayer({ - 'id': String(polygonObj.id), + 'id': layerName, 'type': 'fill', - 'source': String(polygonObj.id), + 'source': sourceName, 'layout': {}, 'paint': { 'fill-color': '#0080ff', From b695cc192120173a4ec586c3b4dbb162d7b37142 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:23:14 +0200 Subject: [PATCH 14/31] update which markers are still selected after clearing the active saved poly --- corehq/apps/geospatial/static/geospatial/js/models.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/corehq/apps/geospatial/static/geospatial/js/models.js b/corehq/apps/geospatial/static/geospatial/js/models.js index a7643f65bb32..c428d9fd20b9 100644 --- a/corehq/apps/geospatial/static/geospatial/js/models.js +++ b/corehq/apps/geospatial/static/geospatial/js/models.js @@ -744,6 +744,8 @@ hqDefine('geospatial/js/models', [ self.selectedSavedPolygonId(''); self.clearActivePolygon(); updateSelectedSavedPolygonParam(); + const features = mapObj.drawControls.getAll().features; + self.mapObj.selectAllMapItems(features); }; function clearDisbursementBeforeProceeding() { From 5cdcb2df2bbef2fe9e9c9594b0758db90a07d866 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:23:35 +0200 Subject: [PATCH 15/31] make open and close even funcs optional --- corehq/apps/geospatial/static/geospatial/js/utils.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/corehq/apps/geospatial/static/geospatial/js/utils.js b/corehq/apps/geospatial/static/geospatial/js/utils.js index c48d24ccf1b1..3235601b188c 100644 --- a/corehq/apps/geospatial/static/geospatial/js/utils.js +++ b/corehq/apps/geospatial/static/geospatial/js/utils.js @@ -28,9 +28,13 @@ hqDefine('geospatial/js/utils', [], function () { popupDiv.setAttribute("data-bind", "template: 'select-case'"); const popup = new mapboxgl.Popup({ offset: 25, anchor: "bottom" }) // eslint-disable-line no-undef .setLngLat(coordinates) - .setDOMContent(popupDiv) - .on('open', openEventFunc) - .on('close', closeEventFunc); + .setDOMContent(popupDiv); + if (openEventFunc) { + popup.on('open', openEventFunc); + } + if (closeEventFunc) { + popup.on('close', closeEventFunc); + } return popup; }; From dd24468824103ddfa8782bde135a07d2f5e5f2a9 Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:25:16 +0200 Subject: [PATCH 16/31] refactor template to correctly reference link property --- .../templates/geospatial/case_management.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/corehq/apps/geospatial/templates/geospatial/case_management.html b/corehq/apps/geospatial/templates/geospatial/case_management.html index dff21af8fe95..6f7357c80539 100644 --- a/corehq/apps/geospatial/templates/geospatial/case_management.html +++ b/corehq/apps/geospatial/templates/geospatial/case_management.html @@ -153,7 +153,7 @@ - +
{% trans "Username" %}
@@ -176,7 +176,7 @@ - +
{% trans "Username" %}
@@ -202,7 +202,7 @@ - +
{% trans "Case Name" %}
@@ -225,7 +225,7 @@ - +
{% trans "Case Name" %}
@@ -239,7 +239,7 @@
- +
{% endblock %} From 6bda15255a60e107847d7002354a4303e0753c1c Mon Sep 17 00:00:00 2001 From: Zandre Engelbrecht Date: Wed, 18 Dec 2024 11:26:16 +0200 Subject: [PATCH 17/31] ran prettify-changed.sh to fix existing code style issues --- .../templates/geospatial/case_management.html | 502 +++++++++++------- 1 file changed, 296 insertions(+), 206 deletions(-) diff --git a/corehq/apps/geospatial/templates/geospatial/case_management.html b/corehq/apps/geospatial/templates/geospatial/case_management.html index 6f7357c80539..90827613189b 100644 --- a/corehq/apps/geospatial/templates/geospatial/case_management.html +++ b/corehq/apps/geospatial/templates/geospatial/case_management.html @@ -2,244 +2,334 @@ {% load i18n %} {% block reportcontent %} -{% include 'geospatial/partials/index_alert.html' %} -
+ {% include 'geospatial/partials/index_alert.html' %} +
- - {% trans "Mobile Worker Filters" %} - -
- - {% blocktrans %} - There was an issue retrieving mobile workers from the server. - If this problem continues, please - report an issue. - {% endblocktrans %} + {% trans "Mobile Worker Filters" %} +
+ + {% blocktrans %} + There was an issue retrieving mobile workers from the + server. + If this problem continues, please + report an issue. + {% endblocktrans %} +
+
+
+ +
+ +
-
-
- -
- -
-
-
-
-
- -
- - -

- - {% blocktrans %} - Only users at this location will be shown on the map. - {% endblocktrans %} -

-
-
-
-
-
- + enable: $root.shouldShowUsers" + > +

+ + {% blocktrans %} + Only users at this location will be shown on the map. + {% endblocktrans %} +

+
+ +
+
+ +
-
-{% include 'geospatial/partials/saved_polygon_filter.html' with uses_disbursement='true' %} -
-

- - {% trans "Running disbursement algorithm..." %} -

-
-
-
-
- {% blocktrans %} - We couldn't match every case to a user with the current disbursement settings. - Please follow any of the below steps to rectify this. -
    -
  • Ensure that your settings are correct
  • -
  • Allocate more users to the area
  • -
  • Use filtered areas to reduce the number of cases such that the algorithm can ensure all cases are assigned.
  • -
- {% endblocktrans %} + {% include 'geospatial/partials/saved_polygon_filter.html' with uses_disbursement='true' %} +
+

+ + {% trans "Running disbursement algorithm..." %} +

-
- - -
- {% block reporttable %} - {% if report.needs_filters %} - {% include 'reports/partials/bootstrap3/description.html' %} - {% else %} - -
- {% endif %} - {% endblock reporttable %} -
+ +
+ {% block reporttable %} + {% if report.needs_filters %} + {% include 'reports/partials/bootstrap3/description.html' %} + {% else %} +
+ {% endif %} + {% endblock reporttable %} +
-
+
- - - {% include 'geospatial/partials/review_assignment_modal.html' %} + + + {% include 'geospatial/partials/review_assignment_modal.html' %}
- -