diff --git a/cypress/elements/event_layer.js b/cypress/elements/event_layer.js index 24b2216f63..7f325331e9 100644 --- a/cypress/elements/event_layer.js +++ b/cypress/elements/event_layer.js @@ -17,7 +17,7 @@ export class EventLayer extends Layer { } selectCoordinate(coordinate) { - cy.get('[data-test="coordinatefield"]').click() + cy.get('[data-test="coordinatefield"]').first().click() cy.contains(coordinate).click() return this diff --git a/cypress/integration/layers/eventlayer.cy.js b/cypress/integration/layers/eventlayer.cy.js index a4a84fb83e..01c08e2e5e 100644 --- a/cypress/integration/layers/eventlayer.cy.js +++ b/cypress/integration/layers/eventlayer.cy.js @@ -4,6 +4,7 @@ import { CURRENT_YEAR, EXTENDED_TIMEOUT, POPUP_WAIT, + getDhis2Version, } from '../../support/util.js' const programE2E = { @@ -26,23 +27,55 @@ const programIP = { const programGeowR = { name: 'E2E - GeoProgram - Points (with reg)', stage: 'E2E - Geo - Stage - Point', - coordinates: [ - { name: 'Event location', coords: '-11.499252, 8.178188' }, - { name: 'Enrollment location', coords: '-11.634007, 8.011976' }, - { name: 'Tracked entity location', coords: '-11.529636, 8.040193' }, + startDate: `2025-01-01`, + endDate: `2025-03-31`, + ous: ['Bo'], + scenarios: [ { - name: 'E2E - Geo - DE - Coordinate', - coords: '-11.602850, 8.077288', + ous: ['Bo', 'Bargbe'], + filters: { item: 'E2E - Geo - DE - ID', value: '#C' }, + coordinates: [ + { name: 'Event location', coords: '-11.499252, 8.178188' }, + { name: 'Enrollment location', coords: '-11.634007, 8.011976' }, + { + name: 'Tracked entity location', + coords: '-11.529636, 8.040193', + }, + { + name: 'E2E - Geo - DE - Coordinate', + coords: '-11.602850, 8.077288', + }, + { + name: 'E2E - Geo - TEA - Coordinate', + coords: '-11.499982, 8.049881', + }, + ], }, { - name: 'E2E - Geo - TEA - Coordinate', - coords: '-11.499982, 8.049881', + ous: ['Bo', 'Badjia', 'Ngelehun CHC'], + filters: { item: 'E2E - Geo - DE - ID', value: 'C' }, + coordinates: [ + { + name: 'Organisation unit location', + coords: '-11.419700, 8.103900', // Ngelehun CHC + }, + ], + }, + { + ous: ['Bo', 'Badjia'], + filters: { item: 'E2E - Geo - DE - ID', value: 'C' }, + coordinates: [ + { + name: 'E2E - Geo - DE - Organisation Unit', + coords: '-11.686100, 7.390850', // Bathurst MCHP + }, + { + name: 'E2E - Geo - TEA - Organisation Unit', + coords: '-11.686100, 7.390850', // Bathurst MCHP + }, + ], }, ], - startDate: `2025-01-01`, - endDate: `2025-03-31`, - ous: ['Bo', 'Bargbe'], - filters: { item: 'E2E - Geo - DE - ID', value: '#C' }, } context('Event Layers', () => { @@ -206,36 +239,81 @@ context('Event Layers', () => { } // Event layer config - Layer.openDialog('Events') .selectProgram(programGeowR.name) .selectStage(programGeowR.stage) - Layer.selectTab('Period') .selectPeriodType({ periodType: 'Start/end dates' }) .typeStartDate(programGeowR.startDate) .typeEndDate(programGeowR.endDate) + Layer.selectTab('Style').selectViewAllEvents() + + // Test coordinates in scenario 0 Layer.selectTab('Org Units') .unselectOu('Sierra Leone') - .openOu(programGeowR.ous[0]) - .selectOu(programGeowR.ous[1]) + .openOu(programGeowR.scenarios[0].ous[0]) + .selectOu(programGeowR.scenarios[0].ous[1]) + Layer.selectTab('Filter') + cy.contains('Add filter').click() + cy.getByDataTest('dhis2-uicore-select-input').last().click() + cy.contains(programGeowR.scenarios[0].filters.item).click() + cy.getByDataTest('dhis2-uiwidgets-inputfield-content') + .find('input') + .type(programGeowR.scenarios[0].filters.value) + testCoordinate(programGeowR.scenarios[0].coordinates[3], false) // Geo - DataElement - Coordinate + testCoordinate(programGeowR.scenarios[0].coordinates[4]) // Geo - TrackedEntityAttribute - Coordinate + testCoordinate(programGeowR.scenarios[0].coordinates[2]) // Tracked entity location + testCoordinate(programGeowR.scenarios[0].coordinates[1]) // Enrollment location + testCoordinate(programGeowR.scenarios[0].coordinates[0]) // Event location + + cy.getByDataTest('layer-edit-button').click() + Layer.selectTab('Filter') + cy.getByDataTest('remove-filter-button').click() + Layer.updateMap() + cy.getByDataTest('layer-edit-button').click() Layer.selectTab('Filter') cy.contains('Add filter').click() cy.getByDataTest('dhis2-uicore-select-input').last().click() - cy.contains(programGeowR.filters.item).click() + cy.contains(programGeowR.scenarios[1].filters.item).click() cy.getByDataTest('dhis2-uiwidgets-inputfield-content') .find('input') - .type(programGeowR.filters.value) + .type(programGeowR.scenarios[1].filters.value) + Layer.updateMap() - Layer.selectTab('Style').selectViewAllEvents() + // Test coordinates in scenario 1 - // Test different coordinates - testCoordinate(programGeowR.coordinates[3], false) // Geo - DataElement - Coordinate - testCoordinate(programGeowR.coordinates[4]) // Geo - TrackedEntityAttribute - Coordinate - testCoordinate(programGeowR.coordinates[2]) // Tracked entity location - testCoordinate(programGeowR.coordinates[1]) // Enrollment location - testCoordinate(programGeowR.coordinates[0]) // Event location + cy.getByDataTest('layer-edit-button').click() + Layer.selectTab('Org Units') + .unselectOu(programGeowR.scenarios[0].ous[1]) + .openOu(programGeowR.scenarios[1].ous[1]) + .selectOu(programGeowR.scenarios[1].ous[2]) + Layer.updateMap() + + testCoordinate(programGeowR.scenarios[1].coordinates[0]) // Organisation Unit location + + // VERSION-TOGGLE + // https://dhis2.atlassian.net/browse/DHIS2-19010 and: + // - [2.40.8] https://github.com/dhis2/dhis2-core/commit/f2286a5aa70b2957bd24925776e9394cd67d44c1 + // - [2.41.4] https://github.com/dhis2/dhis2-core/commit/19f29f27385cfae1c7fac234439f49987ec2abe4 + // - [2.42.0] https://github.com/dhis2/dhis2-core/commit/e5b29f4f1dbee791be9e6befb8a304151a1661c9 + const serverVersion = getDhis2Version() + if ( + (serverVersion.minor === 40 && serverVersion.patch >= 8) || + (serverVersion.minor === 41 && serverVersion.patch >= 4) || + serverVersion.minor >= 42 + ) { + // Test coordinates in scenario 2 + + cy.getByDataTest('layer-edit-button').click() + Layer.selectTab('Org Units') + .unselectOu(programGeowR.scenarios[1].ous[2]) + .selectOu(programGeowR.scenarios[2].ous[1]) + Layer.updateMap() + + testCoordinate(programGeowR.scenarios[2].coordinates[0]) // Geo - DataElement - Organisation Unit + testCoordinate(programGeowR.scenarios[2].coordinates[1]) // Geo - TrackedEntityAttribute - Organisation Unit + } }) }) diff --git a/cypress/support/requests.js b/cypress/support/requests.js index a4df8b94bc..55ab5b33e1 100644 --- a/cypress/support/requests.js +++ b/cypress/support/requests.js @@ -64,7 +64,7 @@ const requests = { }, getCachedDataProvider_SystemSettings: { method: 'GET', - url: '**/systemSettings?key=keyAnalysisRelativePeriod,keyHideDailyPeriods,keyHideWeeklyPeriods,keyHideBiWeeklyPeriods,keyHideMonthlyPeriods,keyHideBiMonthlyPeriods,keyDefaultBaseMap,keyBingMapsApiKey,keyAzureMapsApiKey', + url: '**/systemSettings?key=keyAnalysisRelativePeriod,keyHideDailyPeriods,keyHideWeeklyPeriods,keyHideBiWeeklyPeriods,keyHideMonthlyPeriods,keyHideBiMonthlyPeriods,keyDefaultBaseMap,orgUnitCentroidsInEventsAnalytics,keyBingMapsApiKey,keyAzureMapsApiKey', }, getCachedDataProvider_ExternalMapLayers: { method: 'GET', diff --git a/docs/maps.md b/docs/maps.md index b1a383ca82..56ef355eb1 100644 --- a/docs/maps.md +++ b/docs/maps.md @@ -509,9 +509,10 @@ layer** selection. This opens the Events layer configuration dialog. If there is only one stage available for the selected program, the stage is automatically selected. - - Select a value from the **Coordinate field** for the positions - shown on the map. By default, "Event location" is selected. - Depending on the program, other options may include "Tracked entity location", "Enrollment location", and coordinate-type data elements or attributes such as "Household location". + - Select a value from the **Coordinate field** to determine which positions are displayed + on the map. By default, "Event location" is selected. You can also choose "Organisation unit location". + Depending on the selected program, additional options may include "Tracked entity location", "Enrollment location", and coordinate-type/organisation-unit-type data elements or attributes such as "Household location"/"Referral facility". + The number of events represented on the map may vary depending on the selected option and the availability of coordinates. Organisation units are represented by their centroids. - By default all events with coordinates are shown on the map. Use the **Event status** field to only show events having one diff --git a/i18n/en.pot b/i18n/en.pot index 14154c9668..c61e412772 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-07-08T14:01:49.308Z\n" -"PO-Revision-Date: 2025-07-08T14:01:49.308Z\n" +"POT-Creation-Date: 2025-07-18T13:42:06.919Z\n" +"PO-Revision-Date: 2025-07-18T13:42:06.919Z\n" msgid "2020" msgstr "2020" @@ -95,17 +95,17 @@ msgstr "None" msgid "Cascading" msgstr "Cascading" +msgid "Org Unit location" +msgstr "Org Unit location" + msgid "Event location" msgstr "Event location" msgid "Enrollment location" msgstr "Enrollment location" -msgid "Tracked entity location" -msgstr "Tracked entity location" - -msgid "Organisation unit location" -msgstr "Organisation unit location" +msgid "Tracked Entity location" +msgstr "Tracked Entity location" msgid "Fallback coordinate field" msgstr "Fallback coordinate field" @@ -113,11 +113,11 @@ msgstr "Fallback coordinate field" msgid "Coordinate field" msgstr "Coordinate field" -msgid "Enrollment > event > tracked entity > org unit coordinate" -msgstr "Enrollment > event > tracked entity > org unit coordinate" +msgid "Event > Enrollment > Tracked Entity > Org Unit location" +msgstr "Event > Enrollment > Tracked Entity > Org Unit location" -msgid "Event > org unit coordinate" -msgstr "Event > org unit coordinate" +msgid "Event > Org Unit location" +msgstr "Event > Org Unit location" msgid "Event data item" msgstr "Event data item" @@ -431,6 +431,9 @@ msgstr "Event status" msgid "Boundary color" msgstr "Boundary color" +msgid "Draws a buffer area around each location." +msgstr "Draws a buffer area around each location." + msgid "Buffer" msgstr "Buffer" @@ -452,6 +455,12 @@ msgstr "Line/stroke width" msgid "Line/stroke width must be between 0-10." msgstr "Line/stroke width must be between 0-10." +msgid "Convert polygons to their centroids." +msgstr "Convert polygons to their centroids." + +msgid "Centroids" +msgstr "Centroids" + msgid "Labels" msgstr "Labels" @@ -751,6 +760,9 @@ msgstr "Not set" msgid "Could not retrieve tracked entity data" msgstr "Could not retrieve tracked entity data" +msgid "Tracked entity location" +msgstr "Tracked entity location" + msgid "Last updated" msgstr "Last updated" @@ -1400,6 +1412,9 @@ msgstr "Choropleth" msgid "Bubble map" msgstr "Bubble map" +msgid "Organisation unit location" +msgstr "Organisation unit location" + msgid "Automatic color legend" msgstr "Automatic color legend" diff --git a/jest.config.js b/jest.config.js index bc62416331..af3efd1d9c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.js'], testPathIgnorePatterns: ['/node_modules/', '/cypress/'], transformIgnorePatterns: [ - '/node_modules/(?!d3-(array|axis|color|format|interpolate|scale|selection|time)|internmap)', + '/node_modules/(?!d3-(array|axis|color|format|geo|interpolate|scale|selection|time)|internmap)', ], moduleNameMapper: { '\\.(css)$': 'identity-obj-proxy', diff --git a/package.json b/package.json index 04bae64b89..630357da38 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "d3-axis": "^3.0.0", "d3-color": "^3.1.0", "d3-format": "^3.1.0", + "d3-geo": "^3.1.1", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-time": "^3.1.0", diff --git a/src/actions/layerEdit.js b/src/actions/layerEdit.js index 5d9d18eaff..be3ba7457f 100644 --- a/src/actions/layerEdit.js +++ b/src/actions/layerEdit.js @@ -305,6 +305,12 @@ export const setBufferRadius = (radius) => ({ radius, }) +// Set geometry to centroid (events) +export const setGeometryCentroid = (checked) => ({ + type: types.LAYER_EDIT_GEOMETRY_CENTROIDS_SET, + payload: checked, +}) + // Set point radius low (thematic, org unit) export const setRadiusLow = (radius) => ({ type: types.LAYER_EDIT_RADIUS_LOW_SET, diff --git a/src/components/dataItem/CoordinateField.js b/src/components/dataItem/CoordinateField.js index e000d60f7a..c8e7c1bdd0 100644 --- a/src/components/dataItem/CoordinateField.js +++ b/src/components/dataItem/CoordinateField.js @@ -1,3 +1,4 @@ +import { useConfig } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' import React, { useMemo, useEffect } from 'react' @@ -12,8 +13,6 @@ import { import { useEventDataItems } from '../../hooks/useEventDataItems.js' import { SelectField } from '../core/index.js' -const includeTypes = ['COORDINATE'] - const CoordinateField = ({ value, program, @@ -22,13 +21,32 @@ const CoordinateField = ({ onChange, className, }) => { + const { serverVersion } = useConfig() + + // VERSION-TOGGLE + // https://dhis2.atlassian.net/browse/DHIS2-19010 and: + // - [2.40.8] https://github.com/dhis2/dhis2-core/commit/f2286a5aa70b2957bd24925776e9394cd67d44c1 + // - [2.41.4] https://github.com/dhis2/dhis2-core/commit/19f29f27385cfae1c7fac234439f49987ec2abe4 + // - [2.42.0] https://github.com/dhis2/dhis2-core/commit/e5b29f4f1dbee791be9e6befb8a304151a1661c9 + const includeTypes = ['COORDINATE'] + if ( + (serverVersion.minor === 40 && serverVersion.patch >= 8) || + (serverVersion.minor === 41 && serverVersion.patch >= 4) || + serverVersion.minor >= 42 + ) { + includeTypes.push('ORGANISATION_UNIT') + } + const { eventDataItems, trackedEntityType } = useEventDataItems({ programId: program?.id, programStageId: programStage?.id, includeTypes, }) - const defaultValue = eventCoordinateField ? NONE : EVENT_COORDINATE_DEFAULT + const defaultValue = useMemo( + () => (eventCoordinateField ? NONE : EVENT_COORDINATE_DEFAULT), + [eventCoordinateField] + ) const fields = useMemo(() => { const isFallback = !!eventCoordinateField @@ -45,6 +63,10 @@ const CoordinateField = ({ }) } + fields.push({ + id: EVENT_COORDINATE_ORG_UNIT, + name: i18n.t('Org Unit location'), + }) fields.push({ id: EVENT_COORDINATE_DEFAULT, name: i18n.t('Event location'), @@ -61,7 +83,7 @@ const CoordinateField = ({ }) fields.push({ id: EVENT_COORDINATE_TRACKED_ENTITY, - name: i18n.t('Tracked entity location'), + name: i18n.t('Tracked Entity location'), }) } @@ -69,14 +91,7 @@ const CoordinateField = ({ fields.push(...eventDataItems) } - if (isFallback) { - fields.push({ - id: EVENT_COORDINATE_ORG_UNIT, - name: i18n.t('Organisation unit location'), - }) - } - - return eventCoordinateField + return isFallback ? fields.filter((f) => f.id !== eventCoordinateField) : fields }, [trackedEntityType, eventDataItems, eventCoordinateField]) @@ -86,7 +101,9 @@ const CoordinateField = ({ if ( trackedEntityType && eventDataItems && - !fields.find((f) => f.id === value) + !fields.find((f) => f.id === value) && + value !== defaultValue && + fields.length > 0 ) { onChange(defaultValue) } @@ -108,14 +125,14 @@ const CoordinateField = ({ } items={fields} value={fields.find((f) => f.id === value) ? value : null} - loading={value !== EVENT_COORDINATE_DEFAULT && !trackedEntityType} + loading={!trackedEntityType} helpText={ value === EVENT_COORDINATE_CASCADING ? trackedEntityType ? i18n.t( - 'Enrollment > event > tracked entity > org unit coordinate' + 'Event > Enrollment > Tracked Entity > Org Unit location' ) - : i18n.t('Event > org unit coordinate') + : i18n.t('Event > Org Unit location') : null } onChange={(field) => onChange(field.id)} diff --git a/src/components/edit/event/EventDialog.js b/src/components/edit/event/EventDialog.js index 957c43d622..6866e950ec 100644 --- a/src/components/edit/event/EventDialog.js +++ b/src/components/edit/event/EventDialog.js @@ -11,7 +11,7 @@ import { setEventClustering, setEventPointColor, setEventPointRadius, - // setFallbackCoordinateField, + setFallbackCoordinateField, setPeriod, setStartDate, setEndDate, @@ -51,6 +51,7 @@ import StartEndDate from '../../periods/StartEndDate.js' import ProgramSelect from '../../program/ProgramSelect.js' import ProgramStageSelect from '../../program/ProgramStageSelect.js' import BufferRadius from '../shared/BufferRadius.js' +import GeometryCentroid from '../shared/GeometryCentroid.js' import styles from '../styles/LayerDialog.module.css' import EventStatusSelect from './EventStatusSelect.js' @@ -63,7 +64,7 @@ class EventDialog extends Component { setEventPointColor: PropTypes.func.isRequired, setEventPointRadius: PropTypes.func.isRequired, setEventStatus: PropTypes.func.isRequired, - // setFallbackCoordinateField: PropTypes.func.isRequired, + setFallbackCoordinateField: PropTypes.func.isRequired, setOrgUnits: PropTypes.func.isRequired, setPeriod: PropTypes.func.isRequired, setProgram: PropTypes.func.isRequired, @@ -79,7 +80,7 @@ class EventDialog extends Component { eventPointColor: PropTypes.string, eventPointRadius: PropTypes.number, eventStatus: PropTypes.string, - // fallbackCoordinateField: PropTypes.string, + fallbackCoordinateField: PropTypes.string, filters: PropTypes.array, legendSet: PropTypes.object, method: PropTypes.number, @@ -205,7 +206,7 @@ class EventDialog extends Component { eventCoordinateField, eventPointColor, eventPointRadius, - // fallbackCoordinateField, + fallbackCoordinateField = 'none', filters = [], program, programStage, @@ -222,7 +223,7 @@ class EventDialog extends Component { setEventClustering, setEventPointColor, setEventPointRadius, - // setFallbackCoordinateField, + setFallbackCoordinateField, setPeriod, } = this.props @@ -281,16 +282,17 @@ class EventDialog extends Component { className={styles.select} data-test="eventdialog-coordinatefield" /> - {/* eventCoordinateField && ( - - ) */} + + - - setBufferRadius(isChecked ? radius || defaultRadius : null) - } - /> + + + setBufferRadius( + isChecked ? radius || defaultRadius : null + ) + } + /> + {showBuffer && ( { + useEffect(() => { + setGeometryCentroid( + !EVENT_CENTROID_DEFAULT.includes(eventCoordinateField) + ) + }, [setGeometryCentroid, eventCoordinateField]) + + return ( +
+ + setGeometryCentroid(isChecked)} + /> + +
+ ) +} + +GeometryCentroid.propTypes = { + setGeometryCentroid: PropTypes.func.isRequired, + className: PropTypes.string, + eventCoordinateField: PropTypes.string, + geometryCentroid: PropTypes.bool, +} + +export default connect( + ({ layerEdit }) => ({ + geometryCentroid: layerEdit.geometryCentroid, + eventCoordinateField: layerEdit.eventCoordinateField, + }), + { setGeometryCentroid } +)(GeometryCentroid) diff --git a/src/components/edit/shared/styles/BufferRadius.module.css b/src/components/edit/shared/styles/BufferRadius.module.css index 63c07305c6..55704237fc 100644 --- a/src/components/edit/shared/styles/BufferRadius.module.css +++ b/src/components/edit/shared/styles/BufferRadius.module.css @@ -5,7 +5,7 @@ min-height: 76px; } -.buffer > div { +.buffer > span > div { margin: var(--spacers-dp8); } diff --git a/src/components/edit/shared/styles/GeometryCentroid.module.css b/src/components/edit/shared/styles/GeometryCentroid.module.css new file mode 100644 index 0000000000..cdd74c2b5e --- /dev/null +++ b/src/components/edit/shared/styles/GeometryCentroid.module.css @@ -0,0 +1,9 @@ +.centroid { + margin: 0 -8px; + clear: both; + align-items: center; +} + +.centroid > span > div { + margin: var(--spacers-dp8); +} diff --git a/src/components/filter/RemoveFilter.js b/src/components/filter/RemoveFilter.js index 593f46b2f7..c9f9eee925 100644 --- a/src/components/filter/RemoveFilter.js +++ b/src/components/filter/RemoveFilter.js @@ -6,7 +6,11 @@ import styles from './styles/RemoveFilter.module.css' // Remove filter button used for both thematic and event filters const RemoveFilter = ({ onClick }) => ( -
+
diff --git a/src/components/map/layers/EventPopup.js b/src/components/map/layers/EventPopup.js index 5055c99906..2ce677c77f 100644 --- a/src/components/map/layers/EventPopup.js +++ b/src/components/map/layers/EventPopup.js @@ -19,7 +19,7 @@ const EVENTS_QUERY = { }, } -const getDataRows = ({ displayItems, dataValues }) => { +const getDataRows = ({ displayItems, dataValues, orgUnitNames }) => { const dataRows = [] // Include rows for each data item used for styling and displayInReport @@ -29,6 +29,7 @@ const getDataRows = ({ displayItems, dataValues }) => { value, valueType, options, + orgUnitNames, }) dataRows.push( @@ -57,6 +58,7 @@ const EventPopup = ({ onClose, }) => { const [orgUnit, setOrgUnit] = useState() + const [orgUnitNames, setOrgUnitNames] = useState({}) const { refetch: refetchOrgUnit, fetching: fetchingOrgUnit } = useDataQuery( ORG_UNIT_QUERY, @@ -74,24 +76,41 @@ const EventPopup = ({ }) useEffect(() => { - const fetchEventandOU = async () => { + const fetchEventandOUs = async () => { const resultEvent = await refetchEvent({ id: feature.properties.id || feature.properties[EVENT_ID_FIELD], }) const idOrgUnit = resultEvent?.events?.orgUnit + // Fetch event org unit if (idOrgUnit) { const resultOrgUnit = await refetchOrgUnit({ id: idOrgUnit, nameProperty, }) const nameOrgUnit = resultOrgUnit?.orgUnit?.name - setOrgUnit(nameOrgUnit) } + + // Fetch all org units referenced in displayItems + const orgUnitIds = displayItems + .filter(({ valueType }) => valueType === 'ORGANISATION_UNIT') + .map(({ id }) => { + const { value } = + resultEvent?.events?.dataValues.find( + (d) => d.dataElement === id + ) || {} + return value + }) + const orgUnitsNamesMap = {} + for (const id of orgUnitIds) { + const result = await refetchOrgUnit({ id, nameProperty }) + orgUnitsNamesMap[id] = result?.orgUnit?.name + } + setOrgUnitNames(orgUnitsNamesMap) } - fetchEventandOU() - }, [feature, nameProperty, refetchEvent, refetchOrgUnit]) + fetchEventandOUs() + }, [feature, nameProperty, refetchEvent, refetchOrgUnit, displayItems]) const { type, coordinates: coord } = feature.geometry const { dataValues = [], occurredAt } = dataEvent?.events || {} @@ -131,6 +150,7 @@ const EventPopup = ({ getDataRows({ displayItems, dataValues, + orgUnitNames, })} {type === 'Point' && ( diff --git a/src/components/map/layers/TrackedEntityLayer.js b/src/components/map/layers/TrackedEntityLayer.js index af760ee771..1933062e46 100644 --- a/src/components/map/layers/TrackedEntityLayer.js +++ b/src/components/map/layers/TrackedEntityLayer.js @@ -8,8 +8,7 @@ import { GEOJSON_LAYER, } from '../../../constants/layers.js' import { - GEO_TYPE_POINT, - GEO_TYPE_POLYGON, + getCentroid, GEO_TYPE_LINE, GEO_TYPE_FEATURE, } from '../../../util/geojson.js' @@ -21,33 +20,9 @@ import { import Layer from './Layer.js' import TrackedEntityPopup from './TrackedEntityPopup.js' -const getCentroid = (points) => { - const totals = points.reduce( - (accum, point) => { - accum[0] += point[0] - accum[1] += point[1] - return accum - }, - [0, 0] - ) - return [totals[0] / points.length, totals[1] / points.length] -} - -const geomToCentroid = (geometry) => { - switch (geometry.type) { - case GEO_TYPE_POINT: - return geometry.coordinates - case GEO_TYPE_POLYGON: - // TODO: Support multipolygon / use turf - return getCentroid(geometry.coordinates[0]) - default: - return null - } -} - const makeRelationshipGeometry = ({ from, to }) => { - const fromGeom = geomToCentroid(from.geometry) - const toGeom = geomToCentroid(to.geometry) + const fromGeom = getCentroid(from.geometry) + const toGeom = getCentroid(to.geometry) if (!fromGeom || !toGeom) { // console.error('Invalid relationship geometries', from, to); return null diff --git a/src/components/map/layers/TrackedEntityPopup.js b/src/components/map/layers/TrackedEntityPopup.js index fcec3aa1d2..311d471e1b 100644 --- a/src/components/map/layers/TrackedEntityPopup.js +++ b/src/components/map/layers/TrackedEntityPopup.js @@ -22,7 +22,7 @@ const TRACKED_ENTITIES_QUERY = { }, } -const getDataRows = ({ displayAttributes, attributes }) => { +const getDataRows = ({ displayAttributes, attributes, orgUnitNames }) => { const dataRows = [] // Include rows for each displayInList attribute @@ -32,6 +32,7 @@ const getDataRows = ({ displayAttributes, attributes }) => { value, valueType, options, + orgUnitNames, }) dataRows.push( @@ -60,6 +61,7 @@ const TrackedEntityPopup = ({ onClose, }) => { const [orgUnit, setOrgUnit] = useState() + const [orgUnitNames, setOrgUnitNames] = useState({}) const { refetch: refetchOrgUnit, fetching: fetchingOrgUnit } = useDataQuery( ORG_UNIT_QUERY, @@ -81,24 +83,47 @@ const TrackedEntityPopup = ({ }) useEffect(() => { - const fetchTEandOU = async () => { + const fetchTEandOUs = async () => { const resultTrackedEntity = await refetchTrackedEntity({ id: feature.properties.id, }) const idOrgUnit = resultTrackedEntity?.trackedEntities?.orgUnit + // Fetch trackedEntity org unit if (idOrgUnit) { const resultOrgUnit = await refetchOrgUnit({ id: idOrgUnit, nameProperty, }) const nameOrgUnit = resultOrgUnit?.orgUnit?.name - setOrgUnit(nameOrgUnit) } + + // Fetch all org units referenced in displayAttributes + const orgUnitIds = displayAttributes + .filter(({ valueType }) => valueType === 'ORGANISATION_UNIT') + .map(({ id }) => { + const { value } = + resultTrackedEntity?.trackedEntities?.attributes.find( + (d) => d.attribute === id + ) || {} + return value + }) + const orgUnitsNamesMap = {} + for (const id of orgUnitIds) { + const result = await refetchOrgUnit({ id, nameProperty }) + orgUnitsNamesMap[id] = result?.orgUnit?.name + } + setOrgUnitNames(orgUnitsNamesMap) } - fetchTEandOU() - }, [feature, nameProperty, refetchTrackedEntity, refetchOrgUnit]) + fetchTEandOUs() + }, [ + feature, + nameProperty, + refetchTrackedEntity, + refetchOrgUnit, + displayAttributes, + ]) const { type, coordinates: coord } = feature.geometry const { attributes = [], updatedAt } = @@ -128,6 +153,7 @@ const TrackedEntityPopup = ({ getDataRows({ displayAttributes, attributes, + orgUnitNames, })} {type === 'Point' && ( diff --git a/src/constants/actionTypes.js b/src/constants/actionTypes.js index f7b734061f..1744dce9d5 100644 --- a/src/constants/actionTypes.js +++ b/src/constants/actionTypes.js @@ -127,6 +127,8 @@ export const LAYER_EDIT_LABEL_FONT_WEIGHT_SET = export const LAYER_EDIT_LABEL_FONT_STYLE_SET = 'LAYER_EDIT_LABEL_FONT_STYLE_SET' export const LAYER_EDIT_LABEL_FONT_COLOR_SET = 'LAYER_EDIT_LABEL_FONT_COLOR_SET' export const LAYER_EDIT_BUFFER_RADIUS_SET = 'LAYER_EDIT_BUFFER_RADIUS_SET' +export const LAYER_EDIT_GEOMETRY_CENTROIDS_SET = + 'LAYER_EDIT_GEOMETRY_CENTROIDS_SET' export const LAYER_EDIT_RADIUS_LOW_SET = 'LAYER_EDIT_RADIUS_LOW_SET' export const LAYER_EDIT_RADIUS_HIGH_SET = 'LAYER_EDIT_RADIUS_HIGH_SET' export const LAYER_EDIT_LEGEND_SET_SET = 'LAYER_EDIT_LEGEND_SET_SET' diff --git a/src/constants/layers.js b/src/constants/layers.js index bfdc6f860e..90b8fe1eee 100644 --- a/src/constants/layers.js +++ b/src/constants/layers.js @@ -104,15 +104,21 @@ export const EVENT_COLOR = '#333333' export const EVENT_RADIUS = 6 export const EVENT_BUFFER = 100 export const EVENT_COORDINATE_DEFAULT = 'psigeometry' +export const EVENT_COORDINATE_ORG_UNIT = 'ougeometry' export const EVENT_COORDINATE_ENROLLMENT = 'pigeometry' export const EVENT_COORDINATE_TRACKED_ENTITY = 'teigeometry' -export const EVENT_COORDINATE_ORG_UNIT = 'ougeometry' export const EVENT_COORDINATE_CASCADING = 'cascading' export const COORDINATE_FIELD_NAMES = { [EVENT_COORDINATE_DEFAULT]: i18n.t('Event location'), + [EVENT_COORDINATE_ORG_UNIT]: i18n.t('Organisation unit location'), [EVENT_COORDINATE_ENROLLMENT]: i18n.t('Enrollment location'), [EVENT_COORDINATE_TRACKED_ENTITY]: i18n.t('Tracked entity location'), } +export const EVENT_CENTROID_DEFAULT = [ + EVENT_COORDINATE_DEFAULT, + EVENT_COORDINATE_ENROLLMENT, + EVENT_COORDINATE_TRACKED_ENTITY, +] /* TEI LAYER */ export const TEI_COLOR = '#BB0000' diff --git a/src/constants/settings.js b/src/constants/settings.js index 3dde6d296a..15b828be1f 100644 --- a/src/constants/settings.js +++ b/src/constants/settings.js @@ -15,6 +15,7 @@ export const SYSTEM_SETTINGS = [ 'keyHideMonthlyPeriods', 'keyHideBiMonthlyPeriods', 'keyDefaultBaseMap', + 'orgUnitCentroidsInEventsAnalytics', ...Object.keys(MAP_SERVICE_KEY_TESTS), ] diff --git a/src/constants/valueTypes.js b/src/constants/valueTypes.js index e476691d44..15831f9da8 100644 --- a/src/constants/valueTypes.js +++ b/src/constants/valueTypes.js @@ -34,3 +34,6 @@ export const datetimeValueTypes = ['DATETIME'] // Coordinate value types export const coordinateValueTypes = ['COORDINATE'] + +// Organisation unit value types +export const ouValueTypes = ['ORGANISATION_UNIT'] diff --git a/src/reducers/layerEdit.js b/src/reducers/layerEdit.js index d4564ddbef..a8857a59c7 100644 --- a/src/reducers/layerEdit.js +++ b/src/reducers/layerEdit.js @@ -515,6 +515,12 @@ const layerEdit = (state = null, action) => { areaRadius: action.radius, } + case types.LAYER_EDIT_GEOMETRY_CENTROIDS_SET: + return { + ...state, + geometryCentroid: action.payload, + } + case types.LAYER_EDIT_RADIUS_LOW_SET: return { ...state, diff --git a/src/util/__tests__/geojson.spec.js b/src/util/__tests__/geojson.spec.js index 8aed37779a..9f4b894148 100644 --- a/src/util/__tests__/geojson.spec.js +++ b/src/util/__tests__/geojson.spec.js @@ -1,5 +1,7 @@ import { + CENTROID_FORMAT, getBounds, + getCentroid, addStyleDataItem, createEventFeature, buildEventGeometryGetter, @@ -631,4 +633,106 @@ describe('geojson utils', () => { expect(featureCollection[0].id).toEqual(456) }) }) + + describe('getCentroid', () => { + const polygon = { + type: 'Polygon', + coordinates: [ + [ + [0, 0], + [4, 0], + [4, 4], + [0, 4], + [0, 0], + ], + ], + } + + const multipolygon = { + type: 'MultiPolygon', + coordinates: [ + [ + [ + [0, 0], + [2, 0], + [2, 2], + [0, 2], + [0, 0], + ], + ], + [ + [ + [5, 5], + [7, 5], + [7, 7], + [5, 7], + [5, 5], + ], + ], + ], + } + + const point = { + type: 'Point', + coordinates: [1, 2], + } + + it('returns centroid as array for Polygon', () => { + const centroid = getCentroid(polygon) + expect(Array.isArray(centroid)).toBe(true) + expect(centroid.length).toBe(2) + }) + + it('returns centroid as GeoJSON for Polygon with format=geojson', () => { + const centroid = getCentroid(polygon, CENTROID_FORMAT.GEOJSON) + expect(centroid).toEqual({ + type: 'Point', + coordinates: expect.any(Array), + }) + expect(centroid.coordinates.length).toBe(2) + }) + + it('returns centroid as array for MultiPolygon', () => { + const centroid = getCentroid(multipolygon) + expect(Array.isArray(centroid)).toBe(true) + expect(centroid.length).toBe(2) + }) + + it('returns centroid as GeoJSON for MultiPolygon with format=geojson', () => { + const centroid = getCentroid(multipolygon, CENTROID_FORMAT.GEOJSON) + expect(centroid).toEqual({ + type: 'Point', + coordinates: expect.any(Array), + }) + expect(centroid.coordinates.length).toBe(2) + }) + + it('returns coordinates for Point', () => { + const centroid = getCentroid(point) + expect(centroid).toEqual([1, 2]) + }) + + it('returns GeoJSON Point for Point with format=geojson', () => { + const centroid = getCentroid(point, CENTROID_FORMAT.GEOJSON) + expect(centroid).toEqual({ + type: 'Point', + coordinates: [1, 2], + }) + }) + + it('returns null for null geometry', () => { + expect(getCentroid(null)).toBeNull() + }) + + it('returns null for unknown geometry type', () => { + const unknown = { + type: 'LineString', + coordinates: [ + [0, 0], + [1, 1], + ], + } + expect(getCentroid(unknown)).toBeNull() + }) + }) }) diff --git a/src/util/__tests__/helpers.spec.js b/src/util/__tests__/helpers.spec.js index 54b18864fb..f3b5cdce0d 100644 --- a/src/util/__tests__/helpers.spec.js +++ b/src/util/__tests__/helpers.spec.js @@ -1,123 +1,141 @@ import { formatValueForDisplay, sumObjectValues } from '../helpers.js' describe('formatValueForDisplay', () => { - it('returns "0", "1" as is without transformation', () => { - expect(formatValueForDisplay({ value: '0', valueType: 'NUMBER' })).toBe( - '0' - ) - expect(formatValueForDisplay({ value: '1', valueType: 'NUMBER' })).toBe( - '1' - ) - }) - - it('returns "Not set" for null/undefined/empty string', () => { - expect(formatValueForDisplay({ value: null })).toBe('Not set') - expect(formatValueForDisplay({ value: undefined })).toBe('Not set') - expect(formatValueForDisplay({ value: '' })).toBe('Not set') - expect(formatValueForDisplay({ value: 'Not set' })).toBe('Not set') - }) - - it('returns option label if present in options', () => { - const result = formatValueForDisplay({ - value: 'A', - options: { A: 'Option A', B: 'Option B' }, - }) - expect(result).toBe('Option A') - }) - - it('ignores options if value not in options', () => { - const result = formatValueForDisplay({ - value: 'C', - options: { A: 'Option A', B: 'Option B' }, - valueType: 'TEXT', - }) - expect(result).toBe('C') - }) - - it('formats coordinates when valueType is coordinate (string)', () => { - const result = formatValueForDisplay({ - value: '[12.3456781, 98.7654321]', - valueType: 'COORDINATE', - }) - expect(result).toBe('12.345678, 98.765432') - }) - - it('formats coordinates when valueType is coordinate (array of numbers)', () => { - const result = formatValueForDisplay({ - value: [12.3456781, 98.7654321], - valueType: 'COORDINATE', - }) - expect(result).toBe('12.345678, 98.765432') - }) - - it('formats coordinates when valueType is coordinate (array of strings)', () => { - const result = formatValueForDisplay({ - value: ['12.3456781', '98.7654321'], - valueType: 'COORDINATE', - }) - expect(result).toBe('12.345678, 98.765432') - }) - - it('returns raw value when coordinate parsing fails', () => { - const result = formatValueForDisplay({ - value: 'invalid json', - valueType: 'COORDINATE', - }) - expect(result).toBe('invalid json') - }) - - it('formats boolean true/false', () => { - const trueResult = formatValueForDisplay({ - value: 'true', - valueType: 'BOOLEAN', - }) - expect(trueResult).toBe('Yes') - - const falseResult = formatValueForDisplay({ - value: 'false', - valueType: 'BOOLEAN', - }) - expect(falseResult).toBe('No') - }) - - it('returns raw value if boolean is not true/false', () => { - const result = formatValueForDisplay({ - value: 'maybe', - valueType: 'BOOLEAN', - }) - expect(result).toBe('maybe') - }) - - it('formats date', () => { - const result = formatValueForDisplay({ - value: '2025-05-08T00:00:00Z', - valueType: 'DATE', - }) - expect(result).toBe('2025-05-08') - }) - - it('returns raw value if date is too short', () => { - const result = formatValueForDisplay({ - value: '2025-05', - valueType: 'DATE', - }) - expect(result).toBe('2025-05') - }) - - it('formats datetime', () => { - const result = formatValueForDisplay({ - value: '2025-05-08T00:00:00Z', - valueType: 'DATETIME', - }) - expect(result).toBe('2025-05-08 00:00') - }) - - it('returns raw value if datetime is too short', () => { - const result = formatValueForDisplay({ - value: '2025-05-08T00', - valueType: 'DATETIME', - }) - expect(result).toBe('2025-05-08T00') + it.each([ + { + desc: 'returns "0" as is without transformation', + input: { value: '0', valueType: 'NUMBER' }, + expected: '0', + }, + { + desc: 'returns "1" as is without transformation', + input: { value: '1', valueType: 'NUMBER' }, + expected: '1', + }, + { + desc: 'returns "Not set" for null', + input: { value: null }, + expected: 'Not set', + }, + { + desc: 'returns "Not set" for undefined', + input: { value: undefined }, + expected: 'Not set', + }, + { + desc: 'returns "Not set" for empty string', + input: { value: '' }, + expected: 'Not set', + }, + { + desc: 'returns "Not set" for string "Not set"', + input: { value: 'Not set' }, + expected: 'Not set', + }, + { + desc: 'returns option label if present in options', + input: { + value: 'A', + options: { A: 'Option A', B: 'Option B' }, + }, + expected: 'Option A', + }, + { + desc: 'ignores options if value not in options', + input: { + value: 'C', + options: { A: 'Option A', B: 'Option B' }, + valueType: 'TEXT', + }, + expected: 'C', + }, + { + desc: 'formats coordinates (string)', + input: { + value: '[12.3456781, 98.7654321]', + valueType: 'COORDINATE', + }, + expected: '12.345678, 98.765432', + }, + { + desc: 'formats coordinates (array of numbers)', + input: { + value: [12.3456781, 98.7654321], + valueType: 'COORDINATE', + }, + expected: '12.345678, 98.765432', + }, + { + desc: 'formats coordinates (array of strings)', + input: { + value: ['12.3456781', '98.7654321'], + valueType: 'COORDINATE', + }, + expected: '12.345678, 98.765432', + }, + { + desc: 'returns raw value when coordinate parsing fails', + input: { + value: 'invalid json', + valueType: 'COORDINATE', + }, + expected: 'invalid json', + }, + { + desc: 'formats boolean true', + input: { value: 'true', valueType: 'BOOLEAN' }, + expected: 'Yes', + }, + { + desc: 'formats boolean false', + input: { value: 'false', valueType: 'BOOLEAN' }, + expected: 'No', + }, + { + desc: 'returns raw value if boolean is not true/false', + input: { value: 'maybe', valueType: 'BOOLEAN' }, + expected: 'maybe', + }, + { + desc: 'formats date', + input: { value: '2025-05-08T00:00:00Z', valueType: 'DATE' }, + expected: '2025-05-08', + }, + { + desc: 'returns raw value if date is too short', + input: { value: '2025-05', valueType: 'DATE' }, + expected: '2025-05', + }, + { + desc: 'formats datetime', + input: { value: '2025-05-08T00:00:00Z', valueType: 'DATETIME' }, + expected: '2025-05-08 00:00', + }, + { + desc: 'returns raw value if datetime is too short', + input: { value: '2025-05-08T00', valueType: 'DATETIME' }, + expected: '2025-05-08T00', + }, + { + desc: 'returns org unit name if orgUnitNames has the value', + input: { + value: 'ou123', + valueType: 'ORGANISATION_UNIT', + orgUnitNames: { ou123: 'Sierra Leone' }, + }, + expected: 'Sierra Leone', + }, + { + desc: 'returns raw value if orgUnitNames does not have the value', + input: { + value: 'ou999', + valueType: 'ORGANISATION_UNIT', + orgUnitNames: { ou123: 'Sierra Leone' }, + }, + expected: 'ou999', + }, + ])('$desc', ({ input, expected }) => { + expect(formatValueForDisplay(input)).toBe(expected) }) it('returns raw value for other DHIS2 types not specially handled', () => { @@ -137,7 +155,6 @@ describe('formatValueForDisplay', () => { INTEGER_NEGATIVE: '-3', INTEGER_ZERO_OR_POSITIVE: '0', USERNAME: 'jdoe', - ORGANISATION_UNIT: 'Sierra Leone', URL: 'https://dhis2.org', GEOJSON: '{"type":"Point","coordinates":[125.6, 10.1]}', } diff --git a/src/util/favorites.js b/src/util/favorites.js index 5e16e34ff6..9b5271264e 100644 --- a/src/util/favorites.js +++ b/src/util/favorites.js @@ -24,6 +24,7 @@ const validMapProperties = [ const validLayerProperties = [ 'aggregationType', 'areaRadius', + 'geometryCentroid', 'band', 'classes', 'colorHigh', // Deprecated diff --git a/src/util/geojson.js b/src/util/geojson.js index 99578db79e..2436757c5a 100644 --- a/src/util/geojson.js +++ b/src/util/geojson.js @@ -1,7 +1,29 @@ +import { geoPath } from 'd3-geo' import findIndex from 'lodash/findIndex' export const EVENT_ID_FIELD = 'psi' +const TYPE_NUMBER = 'number' +const TYPE_STRING = 'string' + +export const DHIS2_PROP = '__dhis2propertyid__' + +export const GEO_TYPE_POINT = 'Point' +export const GEO_TYPE_POLYGON = 'Polygon' +export const GEO_TYPE_MULTIPOLYGON = 'MultiPolygon' +export const GEO_TYPE_LINE = 'LineString' +export const GEO_TYPE_FEATURE = 'Feature' +const GEO_TYPE_FEATURE_COLLECTION = 'FeatureCollection' + +const rawGeometryTypes = [ + GEO_TYPE_POINT, + GEO_TYPE_LINE, + GEO_TYPE_POLYGON, + 'MultiPoint', + 'MultiLineString', + GEO_TYPE_MULTIPOLYGON, +] + // TODO: Remove name mapping logic, use server params DataIDScheme / OuputIDScheme instead /* eslint-disable max-params */ export const createEventFeature = ( @@ -10,9 +32,14 @@ export const createEventFeature = ( options, event, id, - getGeometry + getGeometry, + geometryCentroid ) => { - const geometry = getGeometry(event) + let geometry = getGeometry(event) + if (geometryCentroid) { + geometry = getCentroid(geometry, CENTROID_FORMAT.GEOJSON) + } + const properties = event.reduce((props, value, i) => { const header = headers[i] let option @@ -69,12 +96,19 @@ export const createEventFeatures = (response, config = {}) => { options, row, row[idCol], - getGeometry + getGeometry, + config.geometryCentroid ) ) // Sort to draw polygons before points - data.sort((feature) => (feature.geometry.type === 'Polygon' ? -1 : 0)) + data.sort((feature) => + [GEO_TYPE_POLYGON, GEO_TYPE_MULTIPOLYGON].includes( + feature.geometry.type + ) + ? -1 + : 0 + ) return { data, names } } @@ -116,10 +150,35 @@ export const getCoordinatesBounds = (coordinates) => ] ) -const TYPE_NUMBER = 'number' -const TYPE_STRING = 'string' +export const CENTROID_FORMAT = { + ARRAY: 'array', + GEOJSON: 'geojson', +} +const path = geoPath() +export const getCentroid = (geometry, format = CENTROID_FORMAT.ARRAY) => { + if (!geometry || !geometry.type) { + return null + } -export const DHIS2_PROP = '__dhis2propertyid__' + let coords + + switch (geometry.type) { + case 'Point': + coords = geometry.coordinates + break + case 'Polygon': + case 'MultiPolygon': + coords = path.centroid(geometry) + break + default: + return null + } + + if (format === CENTROID_FORMAT.GEOJSON) { + return { type: 'Point', coordinates: coords } + } + return coords +} export const getGeojsonDisplayData = (feature) => { const { properties } = feature @@ -151,21 +210,6 @@ export const getGeojsonDisplayData = (feature) => { } }) } -export const GEO_TYPE_POINT = 'Point' -export const GEO_TYPE_POLYGON = 'Polygon' -export const GEO_TYPE_MULTIPOLYGON = 'MultiPolygon' -export const GEO_TYPE_LINE = 'LineString' -export const GEO_TYPE_FEATURE = 'Feature' -const GEO_TYPE_FEATURE_COLLECTION = 'FeatureCollection' - -const rawGeometryTypes = [ - GEO_TYPE_POINT, - GEO_TYPE_LINE, - GEO_TYPE_POLYGON, - 'MultiPoint', - 'MultiLineString', - GEO_TYPE_MULTIPOLYGON, -] // Ensure that we are always working with a FeatureCollection export const buildGeoJsonFeatures = (geoJson) => { diff --git a/src/util/getMigratedMapConfig.js b/src/util/getMigratedMapConfig.js index ce08c5afff..609df57f6b 100644 --- a/src/util/getMigratedMapConfig.js +++ b/src/util/getMigratedMapConfig.js @@ -1,5 +1,5 @@ import { isString, isObject, sortBy } from 'lodash/fp' -import { EXTERNAL_LAYER } from '../constants/layers.js' +import { EXTERNAL_LAYER, EVENT_CENTROID_DEFAULT } from '../constants/layers.js' export const getMigratedMapConfig = (config, defaultBasemapId) => upgradeMapViews( @@ -81,7 +81,9 @@ const upgradeGisAppLayers = (config) => { const upgradeMapViews = (config) => { const needsUpgrade = config.mapViews.some( (view) => - view.layer === 'boundary' || typeof view.colorScale === 'string' + view.layer === 'boundary' || + typeof view.colorScale === 'string' || + view.geometryCentroid === undefined ) if (!needsUpgrade) { @@ -94,6 +96,14 @@ const upgradeMapViews = (config) => { layer = 'orgUnit' } + if ( + view.geometryCentroid === undefined && + view.layer === 'event' && + !EVENT_CENTROID_DEFAULT.includes(view.eventCoordinateField) + ) { + view.geometryCentroid = true + } + let colorScale = view.colorScale if (typeof colorScale === 'string') { const parts = colorScale.split(',') diff --git a/src/util/helpers.js b/src/util/helpers.js index 2b9e32b505..3d5d4bdb71 100644 --- a/src/util/helpers.js +++ b/src/util/helpers.js @@ -6,6 +6,7 @@ import { dateValueTypes, datetimeValueTypes, coordinateValueTypes, + ouValueTypes, } from '../constants/valueTypes.js' const getBaseFields = (withSubscribers) => { @@ -186,7 +187,12 @@ export const hasValue = (value) => // Formats value for display // Ref: https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-master/metadata.html#metadata-attribute-value-type-and-validations -export const formatValueForDisplay = ({ value, valueType, options }) => { +export const formatValueForDisplay = ({ + value, + valueType, + options, + orgUnitNames, +}) => { if (!hasValue(value)) { return i18n.t('Not set') } @@ -198,6 +204,13 @@ export const formatValueForDisplay = ({ value, valueType, options }) => { if (options && hasValue(options[value])) { return options[value] } + if ( + ouValueTypes.includes(valueType) && + orgUnitNames && + hasValue(orgUnitNames[value]) + ) { + return orgUnitNames[value] + } if (coordinateValueTypes.includes(valueType)) { return formatCoordinate(value) } diff --git a/yarn.lock b/yarn.lock index 5752f5afdf..8024b96bc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7089,7 +7089,7 @@ d3-array@1: resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== -"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.2.4: +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== @@ -7123,6 +7123,13 @@ d3-geo@1.7.1: dependencies: d3-array "1" +d3-geo@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.1.tgz#6027cf51246f9b2ebd64f99e01dc7c3364033a4d" + integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q== + dependencies: + d3-array "2.5.0 - 3" + "d3-interpolate@1.2.0 - 3": version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"