Skip to content

Commit dc4e62d

Browse files
committed
WIP for feature state styling
1 parent 12837ed commit dc4e62d

File tree

5 files changed

+98
-15
lines changed

5 files changed

+98
-15
lines changed

api/app/zonal_stats.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _read_zones(
9595
dict
9696
A GeoJSON-style dictionary: {"type": "FeatureCollection", "features": [...]}
9797
"""
98-
98+
print(f"Reading zones file: {zones_filepath}")
9999
# Check if filepath contains .json or .geojson (case insensitive)
100100
filepath_lower = zones_filepath.lower()
101101
if ".json" in filepath_lower or ".geojson" in filepath_lower:
@@ -147,7 +147,7 @@ def _group_zones(
147147
zones_filepath: FilePath, group_by: GroupBy, admin_level: Optional[int] = None, simplify_tolerance: Optional[float] = None
148148
) -> FilePath:
149149
"""Group zones by a key id and merge polygons."""
150-
safe_filename = zones_filepath.replace("/", "_").replace("s3://", "")
150+
safe_filename = zones_filepath.replace("/", "_").replace("s3://", "").replace("parquet", "json")
151151
output_filename: FilePath = "{zones}.{simplify_tolerance}.{group_by}".format(
152152
zones=safe_filename, group_by=group_by, simplify_tolerance=simplify_tolerance
153153
)
@@ -507,6 +507,7 @@ def intersect_area(masked) -> AreaInSqKm:
507507
stats_results = clean_results
508508

509509
if not geojson_out:
510+
print(f"Extracting feature properties for {zones_filepath}")
510511
feature_properties = _extract_features_properties(
511512
zones_filepath, admin_level, simplify_tolerance
512513
)

frontend/src/components/MapView/Layers/AnalysisLayer/index.tsx

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { RefObject, useEffect } from 'react';
12
import { get } from 'lodash';
2-
import { Layer, Source } from 'react-map-gl/maplibre';
3+
import { Layer, MapRef, Source } from 'react-map-gl/maplibre';
34
import { useSelector } from 'react-redux';
45
import { addPopupData } from 'context/tooltipStateSlice';
56
import {
@@ -33,6 +34,7 @@ import {
3334
import { opacitySelector } from 'context/opacityStateSlice';
3435
import { getFormattedDate } from 'utils/date-utils';
3536
import { invertLegendColors } from 'components/MapView/Legends/utils';
37+
import { layersSelector } from 'context/mapStateSlice/selectors';
3638

3739
const layerId = getLayerMapId('analysis');
3840

@@ -166,14 +168,99 @@ function fillPaintData(
166168
};
167169
}
168170

169-
function AnalysisLayer({ before }: { before?: string }) {
171+
function AnalysisLayer({
172+
before,
173+
mapRef,
174+
}: {
175+
before?: string;
176+
mapRef: RefObject<MapRef>;
177+
}) {
170178
// TODO maybe in the future we can try add this to LayerType so we don't need exclusive code in Legends and MapView to make this display correctly
171179
// Currently it is quite difficult due to how JSON focused the typing is. We would have to refactor it to also accept layers generated on-the-spot
172180
const analysisData = useSelector(analysisResultSelector);
173181
const isAnalysisLayerActive = useSelector(isAnalysisLayerActiveSelector);
174182
const opacityState = useSelector(opacitySelector('analysis'));
175183
const invertedColorsForAnalysis = useSelector(invertedColorsSelector);
176184
useMapCallback('click', layerId, undefined, onClick(analysisData));
185+
const layers = useSelector(layersSelector);
186+
const boundaryLayer = layers.find(
187+
layer => layer.id === analysisData?.baselineLayerId,
188+
)?.id;
189+
const boundary =
190+
analysisData && 'boundaryId' in analysisData && analysisData.boundaryId
191+
? getLayerMapId(analysisData.boundaryId)
192+
: before;
193+
194+
const legend =
195+
analysisData && invertedColorsForAnalysis
196+
? invertLegendColors(analysisData.legend)
197+
: analysisData?.legend;
198+
199+
useEffect(() => {
200+
if (
201+
analysisData instanceof BaselineLayerResult &&
202+
analysisData.adminBoundariesFormat === 'pmtiles'
203+
) {
204+
// Step 1: Get a reference to your map
205+
const map = mapRef.current?.getMap();
206+
207+
if (!map) {
208+
return;
209+
}
210+
211+
// Step 2: Define the source and layer IDs for your admin boundary layer
212+
const boundarySourceId = `source-${boundaryLayer}`;
213+
214+
// Step 3: Get all features from the admin boundary layer
215+
// Option 1: Query rendered features (visible in current view)
216+
const features = map.queryRenderedFeatures({ layers: [boundary] });
217+
const { statistic } = analysisData;
218+
// Step 4: Match features with analysisData and set feature state
219+
// Assuming analysisData.featureCollection.features contains your feature data
220+
features.forEach(feature => {
221+
try {
222+
const adminId = feature.properties.dv_adm0_id;
223+
224+
// Find matching data in analysisData feature collection
225+
const matchingFeature = analysisData.featureCollection.features.find(
226+
f => f.dv_adm0_id === adminId,
227+
);
228+
229+
if (matchingFeature) {
230+
// Set feature state based on properties in matchingFeature
231+
map.setFeatureState(
232+
{
233+
source: boundarySourceId,
234+
sourceLayer: boundary,
235+
id: matchingFeature.dv_adm0_id,
236+
},
237+
{
238+
data: matchingFeature[statistic],
239+
selected: true,
240+
},
241+
);
242+
}
243+
} catch (error) {
244+
console.error('Error setting feature state', error);
245+
}
246+
});
247+
248+
// Step 5: Apply styling based on fillPaintData
249+
// Create a legend from your analysisData
250+
251+
const property = statistic; // The property name you set in feature state
252+
253+
// Apply the fill paint style to your layer
254+
map.setPaintProperty(boundary, 'fill-color', {
255+
property,
256+
stops: legendToStops(legend),
257+
type: 'interval',
258+
});
259+
260+
// Set the opacity
261+
map.setPaintProperty(boundary, 'fill-opacity', 0.7);
262+
}
263+
}, [analysisData, mapRef]);
177264

178265
if (!analysisData || !isAnalysisLayerActive) {
179266
return null;
@@ -196,15 +283,6 @@ function AnalysisLayer({ before }: { before?: string }) {
196283
}
197284
})();
198285

199-
const boundary =
200-
'boundaryId' in analysisData && analysisData.boundaryId
201-
? getLayerMapId(analysisData.boundaryId)
202-
: before;
203-
204-
const legend = invertedColorsForAnalysis
205-
? invertLegendColors(analysisData.legend)
206-
: analysisData.legend;
207-
208286
return (
209287
<Source data={analysisData.featureCollection} type="geojson">
210288
<Layer

frontend/src/components/MapView/Map/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ const MapComponent = memo(() => {
244244
),
245245
});
246246
})}
247-
<AnalysisLayer before={firstBoundaryId} />
247+
<AnalysisLayer before={firstBoundaryId} mapRef={mapRef} />
248248
<SelectionLayer before={firstSymbolId} />
249249
<MapTooltip />
250250
</MapGL>

frontend/src/context/analysisResultStateSlice.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ async function createAPIRequestParams(
440440
...maskParams,
441441
// TODO - remove the need for the geojson_out parameters. See TODO in zonal_stats.py.
442442
// TODO - Add back logic
443-
geojson_out: true, // Boolean(geojsonOut),
443+
geojson_out: Boolean(geojsonOut),
444444
intersect_comparison:
445445
exposureValue?.operator && exposureValue.value
446446
? `${exposureValue?.operator}${exposureValue?.value}`
@@ -767,6 +767,7 @@ export const requestAndStoreAnalysis = createAsyncThunk<
767767
// we never use the raw api data besides for debugging. So lets not bother saving it in Redux for production
768768
process.env.NODE_ENV === 'production' ? undefined : aggregateData,
769769
date,
770+
adminBoundaries.format,
770771
);
771772
});
772773

frontend/src/utils/analysis-utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ export class ExposedPopulationResult {
466466
export class BaselineLayerResult {
467467
key: number = Date.now();
468468
featureCollection: FeatureCollection;
469+
adminBoundariesFormat: string;
469470
tableData: TableRow[];
470471
// for debugging purposes only, as its easy to view the raw API response via Redux Devtools. Should be left empty in production
471472
// @ts-ignore: TS6133
@@ -491,9 +492,11 @@ export class BaselineLayerResult {
491492
legend?: LegendDefinition,
492493
rawApiData?: object[],
493494
analysisDate?: ReturnType<Date['getTime']>,
495+
adminBoundariesFormat?: string,
494496
) {
495497
this.featureCollection = featureCollection;
496498
this.tableData = tableData;
499+
this.adminBoundariesFormat = adminBoundariesFormat ?? 'geojson';
497500
this.statistic = statistic;
498501
this.threshold = threshold;
499502
this.legend = baselineLayer.legend ?? legend;

0 commit comments

Comments
 (0)