Skip to content

Commit a0d642f

Browse files
ray-oxdcursoragent
andcommitted
DBC22-6499: update wildfire display requirements
Show wildfires without polygon data, hide Out and Under Control statuses, update map legend wording, and add Fire of Note status definition in panel. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6788ce5 commit a0d642f

11 files changed

Lines changed: 111 additions & 67 deletions

File tree

src/backend/apps/wildfire/enums.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
WILDFIRE_DISPLAY_STATUSES = [
2+
'Out of Control',
3+
'Being Held',
4+
'Fire of Note',
5+
]
6+
7+
WILDFIRE_HIDDEN_STATUSES = {
8+
'Out',
9+
'Under Control',
10+
}

src/backend/apps/wildfire/fields.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ def to_internal_value(self, data):
2323
"id": data['FIRE_NUMBER'],
2424
"name": data['INCIDENT_NAME'],
2525
"reported_date": datetime.datetime.strptime(data['IGNITION_DATE'], "%Y-%m-%dZ").date(),
26+
"size": data['CURRENT_SIZE'],
27+
"status": data['FIRE_STATUS'],
28+
"url": data['FIRE_URL'],
2629
}
2730

2831
return res

src/backend/apps/wildfire/tasks.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import logging
22

33
from apps.feed.client import FeedClient
4+
from apps.shared.enums import CacheKey
5+
from apps.wildfire.enums import WILDFIRE_HIDDEN_STATUSES
46
from apps.wildfire.models import Wildfire
57
from apps.wildfire.serializers import WildfireInternalSerializer
6-
from django.core.exceptions import ObjectDoesNotExist
7-
from apps.shared.enums import CacheKey
88
from django.core.cache import cache
9+
from django.core.exceptions import ObjectDoesNotExist
910

1011
logger = logging.getLogger(__name__)
1112

1213

1314
def populate_wildfire_from_data(wildfire_data):
1415
wildfire_id = wildfire_data.get('id')
1516
wildfire_status = wildfire_data.get('status')
16-
if not wildfire_status or wildfire_status == 'Out':
17+
if not wildfire_status or wildfire_status in WILDFIRE_HIDDEN_STATUSES:
1718
return
1819

1920
try:
@@ -38,20 +39,21 @@ def populate_all_wildfire_data():
3839
wildfire_areas_dict[wildfire_area['id']] = wildfire_area
3940

4041
logger.warning("wildfire area count: %s", len(wildfire_areas_list))
41-
if len(wildfire_areas_list) == 0:
42-
return
4342

44-
# Combine area data with point data
43+
# Combine area data with point data when available
4544
wildfire_data = []
4645
wildfire_points_list = FeedClient().get_wildfire_location_list()['features']
46+
if len(wildfire_points_list) == 0:
47+
return
48+
4749
for wildfire_point in wildfire_points_list:
50+
combined_data = {
51+
'location': wildfire_point['geometry'],
52+
**wildfire_point,
53+
}
4854
if wildfire_point['id'] in wildfire_areas_dict:
49-
wildfire_area = wildfire_areas_dict[wildfire_point['id']]
50-
wildfire_data.append({
51-
'location': wildfire_point['geometry'],
52-
**wildfire_point,
53-
**wildfire_area
54-
})
55+
combined_data.update(wildfire_areas_dict[wildfire_point['id']])
56+
wildfire_data.append(combined_data)
5557

5658
logger.warning("wildfire data count: %s", len(wildfire_data))
5759
if len(wildfire_data) == 0:
@@ -70,4 +72,4 @@ def populate_all_wildfire_data():
7072
Wildfire.objects.exclude(id__in=active_wildfires).delete()
7173

7274
# Rebuild cache
73-
cache.delete(CacheKey.WILDFIRE_LIST)
75+
cache.delete(CacheKey.WILDFIRE_LIST)

src/backend/apps/wildfire/tests/test_data/wildfire_parsed_feed.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,15 @@
7171
'size': 26276.8,
7272
'status': 'Being Held',
7373
'url': 'https://wildfiresituation.nrs.gov.bc.ca/incidents?fireYear=2025&incidentNumber=G70422'
74+
},
75+
{
76+
'location': Point(-123.0, 54.0),
77+
'geometry': Point(-123.0, 54.0),
78+
'id': 'V12345',
79+
'name': 'Point Only Fire',
80+
'reported_date': datetime.date(2025, 7, 1),
81+
'size': 100,
82+
'status': 'Out of Control',
83+
'url': 'https://wildfiresituation.nrs.gov.bc.ca/incidents?fireYear=2025&incidentNumber=V12345'
7484
}
7585
]

src/backend/apps/wildfire/tests/test_wildfire_populate.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from apps.wildfire.models import Wildfire
1111
from apps.wildfire.tasks import populate_all_wildfire_data, populate_wildfire_from_data
1212
from apps.wildfire.tests.test_data.wildfire_parsed_feed import parsed_feed
13-
from django.contrib.gis.geos import MultiPolygon, Point, Polygon
13+
from django.contrib.gis.geos import MultiPolygon, Point
1414
from httpx import HTTPStatusError
1515

1616
# suppress logged error messages to reduce noise
@@ -50,17 +50,9 @@ def setUp(self):
5050
self.parsed_feed = parsed_feed
5151

5252
def test_populate_wildfire_function(self):
53-
# Polygon/Under Control
54-
populate_wildfire_from_data(self.parsed_feed[0])
55-
wildfire_one = Wildfire.objects.get(id='C50627')
56-
assert wildfire_one.id == 'C50627'
57-
assert wildfire_one.url == 'https://wildfiresituation.nrs.gov.bc.ca/incidents?fireYear=2025&incidentNumber=C50627'
58-
assert wildfire_one.name == 'Martin Lake'
59-
assert isinstance(wildfire_one.location, Point)
60-
assert isinstance(wildfire_one.geometry, Polygon)
61-
assert wildfire_one.size == 2244.5
62-
assert wildfire_one.status == "Under Control"
63-
assert wildfire_one.reported_date == datetime.date(2025, 6, 15)
53+
# Under Control, not populated
54+
assert populate_wildfire_from_data(self.parsed_feed[0]) is None
55+
assert not Wildfire.objects.filter(id='C50627').exists()
6456

6557
# Out, not populated
6658
populate_wildfire_from_data(self.parsed_feed[1])
@@ -78,6 +70,14 @@ def test_populate_wildfire_function(self):
7870
assert wildfire_two.status == "Being Held"
7971
assert wildfire_two.reported_date == datetime.date(2025, 5, 28)
8072

73+
# Point-only/Out of Control
74+
populate_wildfire_from_data(self.parsed_feed[3])
75+
wildfire_three = Wildfire.objects.get(id='V12345')
76+
assert wildfire_three.name == 'Point Only Fire'
77+
assert isinstance(wildfire_three.location, Point)
78+
assert isinstance(wildfire_three.geometry, Point)
79+
assert wildfire_three.status == "Out of Control"
80+
8181
@patch("httpx.get")
8282
def test_populate_and_update_wildfires(self, mock_requests_get):
8383
mock_requests_get.side_effect = [
@@ -90,10 +90,7 @@ def test_populate_and_update_wildfires(self, mock_requests_get):
9090
populate_all_wildfire_data()
9191

9292
# validate data
93-
assert Wildfire.objects.count() == 2 # wildfires with "Out" not populated
94-
wildfire_one = Wildfire.objects.get(id='C50627')
95-
assert wildfire_one.reported_date == datetime.date(2025, 6, 15)
96-
93+
assert Wildfire.objects.count() == 1 # only Being Held wildfires are populated
9794
wildfire_two = Wildfire.objects.get(id='G70422')
9895
assert wildfire_two.reported_date == datetime.date(2025, 5, 28)
9996

src/backend/apps/wildfire/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from apps.shared.enums import CacheKey, CacheTimeout
22
from apps.shared.views import CachedListModelMixin
3+
from apps.wildfire.enums import WILDFIRE_DISPLAY_STATUSES
34
from apps.wildfire.models import Wildfire
45
from apps.wildfire.serializers import WildfireSerializer
56
from rest_framework import viewsets
67

78

89
class WildfireAPI(CachedListModelMixin):
9-
queryset = Wildfire.objects.filter(status__in=['Out of Control', 'Being Held', 'Fire of Note'])
10+
queryset = Wildfire.objects.filter(status__in=WILDFIRE_DISPLAY_STATUSES)
1011
serializer_class = WildfireSerializer
1112
cache_key = CacheKey.WILDFIRE_LIST
1213
cache_timeout = CacheTimeout.WILDFIRE_LIST

src/frontend/src/Components/map/filter/Legend.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export default function Legend() {
171171
{getLegendItem(
172172
wildfiresStaticIcon,
173173
'Wildfires',
174-
'Active forest fires that may impact drivability and are near a road or population area.',
174+
'Active forest fires that are important to note, out of control, or currently being held.',
175175
'major'
176176
)}
177177

src/frontend/src/Components/map/handlers/click.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,11 @@ export const resetClickedStates = (
160160
clickedFeatureRef.current.set('clicked', false);
161161

162162
// Alt feature
163-
clickedFeatureRef.current.get('altFeature').setStyle((isCentroid ? wildfireAreaStyles['static'] : wildfireCentroidStyles['static']));
164-
clickedFeatureRef.current.get('altFeature').set('clicked', false);
163+
const altFeature = clickedFeatureRef.current.get('altFeature');
164+
if (altFeature) {
165+
altFeature.setStyle((isCentroid ? wildfireAreaStyles['static'] : wildfireCentroidStyles['static']));
166+
altFeature.set('clicked', false);
167+
}
165168

166169
updateClickedFeature(null);
167170
}
@@ -186,9 +189,9 @@ export const resetClickedStates = (
186189
break;
187190
}
188191
if (isCamDetail && targetFeature && targetFeature.get('type') === 'camera') {
189-
if (highlighted_camera_list.length > 0) {
192+
if (highlighted_camera_list.length > 0) {
190193
highlighted_camera_list[0].setCameraStyle('static');
191-
highlighted_camera_list[0].set('clicked', false);
194+
highlighted_camera_list[0].set('clicked', false);
192195
}
193196
}
194197
}
@@ -387,7 +390,7 @@ const regionalClickHandler = (
387390
updateClickedFeature,
388391
isCamDetail,
389392
);
390-
393+
391394
}
392395
else {
393396
highlighted_camera_list.push(clickedFeatureRef.current);
@@ -555,8 +558,11 @@ export const wildfireClickHandler = (
555558
feature.set('clicked', true);
556559

557560
// alt feature
558-
feature.get('altFeature').setStyle((isCentroidFeature ? wildfireAreaStyles['active'] : wildfireCentroidStyles['active']));
559-
feature.get('altFeature').set('clicked', true);
561+
const altFeature = feature.get('altFeature');
562+
if (altFeature) {
563+
altFeature.setStyle((isCentroidFeature ? wildfireAreaStyles['active'] : wildfireCentroidStyles['active']));
564+
altFeature.set('clicked', true);
565+
}
560566

561567
updateClickedFeature(feature);
562568
};

src/frontend/src/Components/map/handlers/hover.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ export const resetHoveredStates = (targetFeature, hoveredFeatureRef) => {
115115
{
116116
const isCentroid = hoveredFeature.getGeometry().getType() === 'Point';
117117
hoveredFeature.setStyle((isCentroid ? wildfireCentroidStyles['static'] : wildfireAreaStyles['static']));
118-
hoveredFeature.get('altFeature').setStyle((isCentroid ? wildfireAreaStyles['static'] : wildfireCentroidStyles['static']));
118+
const altFeature = hoveredFeature.get('altFeature');
119+
if (altFeature) {
120+
altFeature.setStyle((isCentroid ? wildfireAreaStyles['static'] : wildfireCentroidStyles['static']));
121+
}
119122
}
120123
break;
121124
case 'dms':
@@ -240,7 +243,10 @@ export const pointerMoveHandler = (e, mapRef, hoveredFeature) => {
240243
if (!targetFeature.get('clicked')) {
241244
const isCentroid = targetFeature.getGeometry().getType() === 'Point';
242245
targetFeature.setStyle((isCentroid ? wildfireCentroidStyles['hover'] : wildfireAreaStyles['hover']));
243-
targetFeature.get('altFeature').setStyle((isCentroid ? wildfireAreaStyles['hover'] : wildfireCentroidStyles['hover']));
246+
const altFeature = targetFeature.get('altFeature');
247+
if (altFeature) {
248+
altFeature.setStyle((isCentroid ? wildfireAreaStyles['hover'] : wildfireCentroidStyles['hover']));
249+
}
244250
}
245251
return;
246252
case 'dms':

src/frontend/src/Components/map/layers/wildfiresLayer.js

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,38 +41,45 @@ export function getWildfiresLayer(
4141
// feature ID to advisory ID for retrieval
4242
locationFeatureForMap.setId(wildfire.id.toString() + '-location');
4343

44-
// Area feature
45-
const isPolygon = wildfire.geometry.type === 'Polygon'
46-
const olGeometry = isPolygon ?
47-
new Polygon(wildfire.geometry.coordinates) :
48-
new MultiPolygon(wildfire.geometry.coordinates);
49-
50-
const areaFeature = new ol.Feature({ geometry: olGeometry, type: 'wildfire' });
51-
52-
// Transfer properties
53-
areaFeature.set('data', wildfire);
54-
55-
// Transform the projection
56-
const areaFeatureForMap = transformFeature(
57-
areaFeature,
58-
'EPSG:4326',
59-
projectionCode,
60-
);
61-
62-
// feature ID to advisory ID for retrieval
63-
areaFeatureForMap.setId(wildfire.id.toString() + '-area');
64-
65-
// Set reference to each other and add to vector source
66-
locationFeatureForMap.set('altFeature', areaFeatureForMap);
67-
areaFeatureForMap.set('altFeature', locationFeatureForMap);
6844
vectorSource.addFeature(locationFeatureForMap);
69-
vectorSource.addFeature(areaFeatureForMap);
45+
46+
// Area feature (only when polygon data is available)
47+
const geometryType = wildfire.geometry?.type;
48+
if (geometryType === 'Polygon' || geometryType === 'MultiPolygon') {
49+
const isPolygon = geometryType === 'Polygon';
50+
const olGeometry = isPolygon ?
51+
new Polygon(wildfire.geometry.coordinates) :
52+
new MultiPolygon(wildfire.geometry.coordinates);
53+
54+
const areaFeature = new ol.Feature({ geometry: olGeometry, type: 'wildfire' });
55+
56+
// Transfer properties
57+
areaFeature.set('data', wildfire);
58+
59+
// Transform the projection
60+
const areaFeatureForMap = transformFeature(
61+
areaFeature,
62+
'EPSG:4326',
63+
projectionCode,
64+
);
65+
66+
// feature ID to advisory ID for retrieval
67+
areaFeatureForMap.setId(wildfire.id.toString() + '-area');
68+
69+
// Set reference to each other and add to vector source
70+
locationFeatureForMap.set('altFeature', areaFeatureForMap);
71+
areaFeatureForMap.set('altFeature', locationFeatureForMap);
72+
vectorSource.addFeature(areaFeatureForMap);
73+
}
7074

7175
// Update the reference feature if id is the reference
7276
if (referenceData?.type === 'wildfire') {
7377
if (wildfire.id == referenceData.id) { // Intentional loose equality for string IDs
74-
updateReferenceFeature(areaFeatureForMap);
7578
updateReferenceFeature(locationFeatureForMap);
79+
const altFeature = locationFeatureForMap.get('altFeature');
80+
if (altFeature) {
81+
updateReferenceFeature(altFeature);
82+
}
7683
}
7784
}
7885
});

0 commit comments

Comments
 (0)