Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Generated by Django 4.2.3 on 2024-05-17 19:45

import django.contrib.gis.db.models.fields
from apps.cms.models import Advisory
from django.db import migrations


def delete_all_advisory_rows(apps, schema_editor):
Advisory = apps.get_model('cms', 'Advisory')
Advisory.objects.all().delete()


Expand Down
4 changes: 4 additions & 0 deletions src/backend/apps/wildfire/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
WILDFIRE_HIDDEN_STATUSES = [
'Out',
'Under Control',
]
3 changes: 3 additions & 0 deletions src/backend/apps/wildfire/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ def to_internal_value(self, data):
"id": data['FIRE_NUMBER'],
"name": data['INCIDENT_NAME'],
"reported_date": datetime.datetime.strptime(data['IGNITION_DATE'], "%Y-%m-%dZ").date(),
"size": data['CURRENT_SIZE'],
"status": data['FIRE_STATUS'],
"url": data['FIRE_URL'],
}

return res
16 changes: 16 additions & 0 deletions src/backend/apps/wildfire/migrations/0003_alter_wildfire_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('wildfire', '0002_alter_wildfire_name'),
]

operations = [
migrations.AlterField(
model_name='wildfire',
name='status',
field=models.CharField(blank=True, default='', max_length=128),
),
]
2 changes: 1 addition & 1 deletion src/backend/apps/wildfire/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ class Wildfire(ExportModelOperationsMixin('wildfire'), BaseModel):
location = models.PointField()

size = models.FloatField()
status = models.CharField(max_length=128)
status = models.CharField(max_length=128, blank=True, default='')

reported_date = models.DateField()
28 changes: 15 additions & 13 deletions src/backend/apps/wildfire/tasks.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import logging

from apps.feed.client import FeedClient
from apps.shared.enums import CacheKey
from apps.wildfire.enums import WILDFIRE_HIDDEN_STATUSES
from apps.wildfire.models import Wildfire
from apps.wildfire.serializers import WildfireInternalSerializer
from django.core.exceptions import ObjectDoesNotExist
from apps.shared.enums import CacheKey
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist

logger = logging.getLogger(__name__)


def populate_wildfire_from_data(wildfire_data):
wildfire_id = wildfire_data.get('id')
wildfire_status = wildfire_data.get('status')
if not wildfire_status or wildfire_status == 'Out':
if wildfire_status in WILDFIRE_HIDDEN_STATUSES:
return

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

logger.warning("wildfire area count: %s", len(wildfire_areas_list))
if len(wildfire_areas_list) == 0:
return

# Combine area data with point data
# Combine area data with point data when available
wildfire_data = []
wildfire_points_list = FeedClient().get_wildfire_location_list()['features']
if len(wildfire_points_list) == 0:
return

for wildfire_point in wildfire_points_list:
combined_data = {
'location': wildfire_point['geometry'],
**wildfire_point,
}
if wildfire_point['id'] in wildfire_areas_dict:
wildfire_area = wildfire_areas_dict[wildfire_point['id']]
wildfire_data.append({
'location': wildfire_point['geometry'],
**wildfire_point,
**wildfire_area
})
combined_data.update(wildfire_areas_dict[wildfire_point['id']])
wildfire_data.append(combined_data)

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

# Rebuild cache
cache.delete(CacheKey.WILDFIRE_LIST)
cache.delete(CacheKey.WILDFIRE_LIST)
10 changes: 10 additions & 0 deletions src/backend/apps/wildfire/tests/test_data/wildfire_parsed_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,15 @@
'size': 26276.8,
'status': 'Being Held',
'url': 'https://wildfiresituation.nrs.gov.bc.ca/incidents?fireYear=2025&incidentNumber=G70422'
},
{
'location': Point(-123.0, 54.0),
'geometry': Point(-123.0, 54.0),
'id': 'V12345',
'name': 'Point Only Fire',
'reported_date': datetime.date(2025, 7, 1),
'size': 100,
'status': 'Out of Control',
'url': 'https://wildfiresituation.nrs.gov.bc.ca/incidents?fireYear=2025&incidentNumber=V12345'
}
]
35 changes: 19 additions & 16 deletions src/backend/apps/wildfire/tests/test_wildfire_populate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from apps.wildfire.models import Wildfire
from apps.wildfire.tasks import populate_all_wildfire_data, populate_wildfire_from_data
from apps.wildfire.tests.test_data.wildfire_parsed_feed import parsed_feed
from django.contrib.gis.geos import MultiPolygon, Point, Polygon
from django.contrib.gis.geos import MultiPolygon, Point
from httpx import HTTPStatusError

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

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

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

# Point-only/Out of Control
populate_wildfire_from_data(self.parsed_feed[3])
wildfire_three = Wildfire.objects.get(id='V12345')
assert wildfire_three.name == 'Point Only Fire'
assert isinstance(wildfire_three.location, Point)
assert isinstance(wildfire_three.geometry, Point)
assert wildfire_three.status == "Out of Control"

# Blank status
blank_status_data = {**self.parsed_feed[3], 'id': 'V12346', 'status': ''}
populate_wildfire_from_data(blank_status_data)
wildfire_four = Wildfire.objects.get(id='V12346')
assert wildfire_four.status == ''

@patch("httpx.get")
def test_populate_and_update_wildfires(self, mock_requests_get):
mock_requests_get.side_effect = [
Expand All @@ -90,10 +96,7 @@ def test_populate_and_update_wildfires(self, mock_requests_get):
populate_all_wildfire_data()

# validate data
assert Wildfire.objects.count() == 2 # wildfires with "Out" not populated
wildfire_one = Wildfire.objects.get(id='C50627')
assert wildfire_one.reported_date == datetime.date(2025, 6, 15)

assert Wildfire.objects.count() == 1 # only Being Held wildfires are populated
wildfire_two = Wildfire.objects.get(id='G70422')
assert wildfire_two.reported_date == datetime.date(2025, 5, 28)

Expand Down
3 changes: 2 additions & 1 deletion src/backend/apps/wildfire/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from apps.shared.enums import CacheKey, CacheTimeout
from apps.shared.views import CachedListModelMixin
from apps.wildfire.enums import WILDFIRE_HIDDEN_STATUSES
from apps.wildfire.models import Wildfire
from apps.wildfire.serializers import WildfireSerializer
from rest_framework import viewsets


class WildfireAPI(CachedListModelMixin):
queryset = Wildfire.objects.filter(status__in=['Out of Control', 'Being Held', 'Fire of Note'])
queryset = Wildfire.objects.exclude(status__in=WILDFIRE_HIDDEN_STATUSES)
serializer_class = WildfireSerializer
cache_key = CacheKey.WILDFIRE_LIST
cache_timeout = CacheTimeout.WILDFIRE_LIST
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/Components/map/filter/Legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export default function Legend() {
{getLegendItem(
wildfiresStaticIcon,
'Wildfires',
'Active forest fires that may impact drivability and are near a road or population area.',
'Active forest fires that are important to note, out of control, or currently being held.',
'major'
)}

Expand Down
20 changes: 13 additions & 7 deletions src/frontend/src/Components/map/handlers/click.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,11 @@ export const resetClickedStates = (
clickedFeatureRef.current.set('clicked', false);

// Alt feature
clickedFeatureRef.current.get('altFeature').setStyle((isCentroid ? wildfireAreaStyles['static'] : wildfireCentroidStyles['static']));
clickedFeatureRef.current.get('altFeature').set('clicked', false);
const altFeature = clickedFeatureRef.current.get('altFeature');
if (altFeature) {
altFeature.setStyle((isCentroid ? wildfireAreaStyles['static'] : wildfireCentroidStyles['static']));
altFeature.set('clicked', false);
}

updateClickedFeature(null);
}
Expand All @@ -186,9 +189,9 @@ export const resetClickedStates = (
break;
}
if (isCamDetail && targetFeature && targetFeature.get('type') === 'camera') {
if (highlighted_camera_list.length > 0) {
if (highlighted_camera_list.length > 0) {
highlighted_camera_list[0].setCameraStyle('static');
highlighted_camera_list[0].set('clicked', false);
highlighted_camera_list[0].set('clicked', false);
}
}
}
Expand Down Expand Up @@ -387,7 +390,7 @@ const regionalClickHandler = (
updateClickedFeature,
isCamDetail,
);

}
else {
highlighted_camera_list.push(clickedFeatureRef.current);
Expand Down Expand Up @@ -555,8 +558,11 @@ export const wildfireClickHandler = (
feature.set('clicked', true);

// alt feature
feature.get('altFeature').setStyle((isCentroidFeature ? wildfireAreaStyles['active'] : wildfireCentroidStyles['active']));
feature.get('altFeature').set('clicked', true);
const altFeature = feature.get('altFeature');
if (altFeature) {
altFeature.setStyle((isCentroidFeature ? wildfireAreaStyles['active'] : wildfireCentroidStyles['active']));
altFeature.set('clicked', true);
}

updateClickedFeature(feature);
};
Expand Down
10 changes: 8 additions & 2 deletions src/frontend/src/Components/map/handlers/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ export const resetHoveredStates = (targetFeature, hoveredFeatureRef) => {
{
const isCentroid = hoveredFeature.getGeometry().getType() === 'Point';
hoveredFeature.setStyle((isCentroid ? wildfireCentroidStyles['static'] : wildfireAreaStyles['static']));
hoveredFeature.get('altFeature').setStyle((isCentroid ? wildfireAreaStyles['static'] : wildfireCentroidStyles['static']));
const altFeature = hoveredFeature.get('altFeature');
if (altFeature) {
altFeature.setStyle((isCentroid ? wildfireAreaStyles['static'] : wildfireCentroidStyles['static']));
}
}
break;
case 'dms':
Expand Down Expand Up @@ -240,7 +243,10 @@ export const pointerMoveHandler = (e, mapRef, hoveredFeature) => {
if (!targetFeature.get('clicked')) {
const isCentroid = targetFeature.getGeometry().getType() === 'Point';
targetFeature.setStyle((isCentroid ? wildfireCentroidStyles['hover'] : wildfireAreaStyles['hover']));
targetFeature.get('altFeature').setStyle((isCentroid ? wildfireAreaStyles['hover'] : wildfireCentroidStyles['hover']));
const altFeature = targetFeature.get('altFeature');
if (altFeature) {
altFeature.setStyle((isCentroid ? wildfireAreaStyles['hover'] : wildfireCentroidStyles['hover']));
}
}
return;
case 'dms':
Expand Down
59 changes: 33 additions & 26 deletions src/frontend/src/Components/map/layers/wildfiresLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,38 +41,45 @@ export function getWildfiresLayer(
// feature ID to advisory ID for retrieval
locationFeatureForMap.setId(wildfire.id.toString() + '-location');

// Area feature
const isPolygon = wildfire.geometry.type === 'Polygon'
const olGeometry = isPolygon ?
new Polygon(wildfire.geometry.coordinates) :
new MultiPolygon(wildfire.geometry.coordinates);

const areaFeature = new ol.Feature({ geometry: olGeometry, type: 'wildfire' });

// Transfer properties
areaFeature.set('data', wildfire);

// Transform the projection
const areaFeatureForMap = transformFeature(
areaFeature,
'EPSG:4326',
projectionCode,
);

// feature ID to advisory ID for retrieval
areaFeatureForMap.setId(wildfire.id.toString() + '-area');

// Set reference to each other and add to vector source
locationFeatureForMap.set('altFeature', areaFeatureForMap);
areaFeatureForMap.set('altFeature', locationFeatureForMap);
vectorSource.addFeature(locationFeatureForMap);
vectorSource.addFeature(areaFeatureForMap);

// Area feature (only when polygon data is available)
const geometryType = wildfire.geometry?.type;
if (geometryType === 'Polygon' || geometryType === 'MultiPolygon') {
const isPolygon = geometryType === 'Polygon';
const olGeometry = isPolygon ?
new Polygon(wildfire.geometry.coordinates) :
new MultiPolygon(wildfire.geometry.coordinates);

const areaFeature = new ol.Feature({ geometry: olGeometry, type: 'wildfire' });

// Transfer properties
areaFeature.set('data', wildfire);

// Transform the projection
const areaFeatureForMap = transformFeature(
areaFeature,
'EPSG:4326',
projectionCode,
);

// feature ID to advisory ID for retrieval
areaFeatureForMap.setId(wildfire.id.toString() + '-area');

// Set reference to each other and add to vector source
locationFeatureForMap.set('altFeature', areaFeatureForMap);
areaFeatureForMap.set('altFeature', locationFeatureForMap);
vectorSource.addFeature(areaFeatureForMap);
}

// Update the reference feature if id is the reference
if (referenceData?.type === 'wildfire') {
if (wildfire.id == referenceData.id) { // Intentional loose equality for string IDs
updateReferenceFeature(areaFeatureForMap);
updateReferenceFeature(locationFeatureForMap);
const altFeature = locationFeatureForMap.get('altFeature');
if (altFeature) {
updateReferenceFeature(altFeature);
}
}
}
});
Expand Down
4 changes: 3 additions & 1 deletion src/frontend/src/Components/map/panels/WildfirePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ export default function WildfirePanel(props) {
const smallScreen = useMediaQuery('only screen and (max-width: 575px)');

const wildfireStatusDescriptions = {
'wildfire of note': 'A wildfire that is creating an increased level of interest. We use this designation to make it easier to find and access response information on the wildfire map. A wildfire that is spreading or it is anticipated to spread beyond the current perimeter, or control line.',
'fire of note': 'A wildfire that is creating an increased level of interest. We use this designation to make it easier to find and access response information on the wildfire map.',
'wildfire of note': 'A wildfire that is creating an increased level of interest. We use this designation to make it easier to find and access response information on the wildfire map.',
'out of control': 'A wildfire that is spreading or it is anticipated to spread beyond the current perimeter, or control line.',
'being held': 'A wildfire that is projected, based on fuel and weather conditions and resource availability, to remain within the current perimeter, control line or boundary.',
'under control': 'A wildfire that is not projected to spread beyond the current perimeter.',
};

const wildfireStatusClasses = {
'fire of note': 'wildfire-status--note',
'wildfire of note': 'wildfire-status--note',
'out of control': 'wildfire-status--out-of-control',
'being held': 'wildfire-status--being-held',
Expand Down
Loading