+
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"