Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues #54, #71, and #90 - Hurricane track fixes #111

Merged
merged 8 commits into from
Jul 2, 2024
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
84 changes: 63 additions & 21 deletions src/components/control-panel/control-panel.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, Fragment } from 'react';
import axios from 'axios';
import { useLayers } from '@context/map-context';
import { useQuery } from '@tanstack/react-query';
Expand Down Expand Up @@ -36,13 +36,15 @@ const layerIcons = {
export const ControlPanel = () => {

const { defaultModelLayers,
hurricaneTrackLayers,
setDefaultModelLayers,
getAllLayersInvisible,
toggleLayerVisibility,
toggleHurricaneLayerVisibility } = useLayers();

const data_url = `${process.env.REACT_APP_UI_DATA_URL}get_ui_data?limit=1&use_v3_sp=true`;
const layers = [...defaultModelLayers];
const hurrLayers = [...hurricaneTrackLayers];

// keep track of which model run to retrieve
let runCycle = "";
Expand All @@ -51,6 +53,9 @@ export const ControlPanel = () => {
let instanceName = "";
let metClass = "";
let eventType = "";
let stormName = "";
let runGrid = "";
let topRunId = "";
let currentLayerSelection = "";

// collect the top layers - those are all we are concerned with
Expand All @@ -74,6 +79,10 @@ export const ControlPanel = () => {
instanceName = topLayers[0].properties.instance_name;
metClass = topLayers[0].properties.met_class;
eventType = topLayers[0].properties.event_type;
runGrid = topLayers[0].properties.grid_type;
stormName = topLayers[0].properties.storm_name;
topRunId = topLayers[0].id.split("-")[0];

// find the current layer selection
const selectedLayer = topLayers.find((layer) => layer.properties.product_type !== "obs" && layer.state.visible === true);
if (selectedLayer) {
Expand All @@ -83,8 +92,10 @@ export const ControlPanel = () => {
const maxele_layer = layers.find((layer) => layer.properties.product_type === "maxele63");
const obs_layer = layers.find((layer) => layer.properties.product_type === "obs");

const [checkedHurr, setCheckedHurr] = React.useState(true);
const [filters, setFilters] = React.useState();
//const [topRunId, setTopRunId] = useState();
const [filters, setFilters] = useState();

// if (topLayers[0]) setTopRunId(topLayers[0].id);

// when cycle buttons are pushed on the control panel
// either the previous or next cycle of the displayed
Expand All @@ -111,6 +122,10 @@ export const ControlPanel = () => {
});
};

const matchNewTropicalRunId = (layer) => {
return (layer.id.split("-")[0] === topRunId);
};

const parseAndAddLayers = (d) => {

// first see if this set of layers already exists in default layers
Expand Down Expand Up @@ -145,10 +160,12 @@ export const ControlPanel = () => {
// add visibity state property to retrieved catalog layers
const newLayers = [];
d.catalog[0].members.forEach((layer) => {
newLayers.push({
...layer,
state: newLayerDefaultState(layer)
});
if (matchNewTropicalRunId(layer)) {
newLayers.push({
...layer,
state: newLayerDefaultState(layer)
});
}
});
setDefaultModelLayers([...newLayers, ...currentLayers]);
}
Expand Down Expand Up @@ -181,12 +198,12 @@ export const ControlPanel = () => {

return new Date(dateParts[0], dateParts[1]-1, dateParts[2]);
};

// switch to the model run layer selected via icon button
const layerChange = async (event, newValue) => {

// turn off the old
layers.map(layer => {
// turn off the old just check for top instance
topLayers.map(layer => {
if (layer.layers.includes(currentLayerSelection)) {
toggleLayerVisibility(layer.id);
}
Expand All @@ -195,7 +212,7 @@ export const ControlPanel = () => {
// Yikes! need another way to do this - but it works for now
await new Promise(r => setTimeout(r, 1));
// turn on the new
layers.map(layer => {
topLayers.map(layer => {
if (layer.layers.includes(newValue)) {
toggleLayerVisibility(layer.id);
}
Expand All @@ -209,8 +226,7 @@ export const ControlPanel = () => {
};

// switch on/off the hurricane track layer, if it exists
const toggleHurricaneLayer = (event) => {
setCheckedHurr(event.target.checked);
const toggleHurricaneLayer = () => {
const layerID = obs_layer.id.substr(0, obs_layer.id.lastIndexOf("-")) + '-hurr';
toggleHurricaneLayerVisibility(layerID);
};
Expand Down Expand Up @@ -244,15 +260,36 @@ export const ControlPanel = () => {
const newFilters = {"instance_name": instanceName,
"met_class": metClass,
"event_type": eventType,
"run_date": runDate,
"cycle": runCycle
"storm_name": stormName,
"cycle": runCycle,
"run_date": runDate
};

setFilters(newFilters);
};

const changeTropicalAdvisory = (direction) => {
console.log(direction);
let currentAdvisory = Number(runAdvisory);

// set properties for next model run
if (direction === "next") {
currentAdvisory += 1;
} else { // previous
// set properties for previous model run
currentAdvisory -= 1;
}

runAdvisory = String(currentAdvisory).padStart(3, '0');

const newFilters = {"instance_name": instanceName,
"met_class": metClass,
"event_type": eventType,
"storm_name": stormName,
"advisory_number": runAdvisory,
"grid_type": runGrid
};

setFilters(newFilters);
};

// cycle to the next/previous model run cycle
Expand Down Expand Up @@ -297,9 +334,14 @@ export const ControlPanel = () => {
<Divider />
{
layers.length && (
<Fragment>
<Typography level="body-md" alignSelf="center">
{metClass === 'tropical'? `Storm Name ${stormName}` : ''}
</Typography>
<Typography level="body-md" alignSelf="center">
Model run date: {runDate}
</Typography>
</Fragment>
)
}

Expand Down Expand Up @@ -337,10 +379,10 @@ export const ControlPanel = () => {
}

{ // hurricane track toggle
layers.some(layer => layer.properties.met_class === "tropical") && (
layers.some(layer => layer.properties.met_class === "tropical") && hurrLayers[0] && (
<Typography
component="label"
endDecorator={ <Switch checked={checkedHurr} onChange={toggleHurricaneLayer} /> }
endDecorator={ <Switch checked={hurrLayers[0].state.visible} onChange={toggleHurricaneLayer} /> }
>Hurricane Track</Typography>
)
}
Expand All @@ -357,7 +399,7 @@ export const ControlPanel = () => {
maxele_layer && (
<IconButton
value={maxele_layer.properties.product_type}
key={maxele_layer.id}
key={Math.random()}
>
{ layerIcons[maxele_layer.properties.product_type] }
</IconButton>
Expand All @@ -366,9 +408,9 @@ export const ControlPanel = () => {
{
topLayers
.filter(layer => layer.properties.product_type !== "obs" && layer.properties.product_type !== "maxele63")
.map(layer => (
.map((layer, index) => (
<IconButton
key={layer.id}
key={Math.random() + index}
value={layer.properties.product_type}
>
{ layerIcons[layer.properties.product_type] }
Expand Down
156 changes: 156 additions & 0 deletions src/components/map/hurricane-track.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, { useState, useEffect, Fragment} from 'react';
import PropTypes from 'prop-types';
import { GeoJSON } from 'react-leaflet';
import { Marker } from 'leaflet';
import {
getTrackData,
getTrackGeojson
} from "@utils/hurricane/track";
import { useLayers } from '@context';


export const HurricaneTrackGeoJson = ({index}) => {

const {
hurricaneTrackLayers,
} = useLayers();
const [hurricaneData, setHurricaneData] = useState();

function coneStyle() {
return {
fillColor: '#858585',
weight: 2,
opacity: 1,
color: '#858585',
fillOpacity: 0.2,
dashArray: '5',
};
}

function lineStyle() {
return {
weight: 2,
opacity: 1,
color: 'red',
};
}

const hurrPointToLayer = ((feature, latlng) => {
const icon_url = `${process.env.REACT_APP_HURRICANE_ICON_URL}`;
let iconName = null;
const L = window.L;
const iconSize = [20, 40];
const iconAnchor = 15;

switch (feature.properties.storm_type) {
default:
case 'TD':
iconName="dep.png";
break;
case 'TS':
iconName="storm.png";
break;
case 'CAT1':
iconName="cat_1.png";
break;
case 'CAT2':
iconName="cat_2.png";
break;
case 'CAT3':
iconName="cat_3.png";
break;
case 'CAT4':
iconName="cat_4.png";
break;
case 'CAT5':
iconName="cat5.png";
break;
}

const url = icon_url + iconName;
const icon = L.icon({
iconUrl: url,
iconSize: [iconSize, iconSize],
iconAnchor: [iconAnchor, iconAnchor],
popupAnchor: [0, 0],
});

return new Marker(latlng, {
icon:icon
});
});

const onEachHurrFeature = (feature, layer) => {
if (feature.properties && feature.properties.time_utc) {
const popupContent = feature.properties.storm_name + ": " +
feature.properties.time_utc + ", " +
feature.properties.max_wind_speed_mph + "mph";

layer.on("mouseover", function (e) {
this.bindPopup(popupContent).openPopup(e.latlng);
});

layer.on("mousemove", function (e) {
this.getPopup().setLatLng(e.latlng);
});

layer.on("mouseout", function () {
this.closePopup();
});
}
};

useEffect(() => {

async function GetStormTrackGeoJson() {

if (hurricaneTrackLayers[index]) {
getTrackData(hurricaneTrackLayers[index].year, hurricaneTrackLayers[index].stormNumber, hurricaneTrackLayers[index].advisory).then((track) => {
if (track != null) {
const trackGeojson = getTrackGeojson(
track,
"utc",
hurricaneTrackLayers[index].stormName
);

// check to make sure we actually got geojson data
// before creating layer
if (trackGeojson) {
setHurricaneData(trackGeojson);
}
}
});
}
}

GetStormTrackGeoJson().then();
}, []);

return(
<Fragment key={Math.random() + "-frag"}>
{hurricaneData && (
<>
<GeoJSON
key={Math.random() + "-cone"}
data={hurricaneData["cone"]}
style={coneStyle}
/>
<GeoJSON
key={Math.random() + "-line"}
data={hurricaneData["line"]}
style={lineStyle}
/>
<GeoJSON
key={Math.random() + "-points"}
data={hurricaneData["points"]}
pointToLayer={hurrPointToLayer}
onEachFeature={onEachHurrFeature}
/>
</>
)};
</Fragment>
);
};
HurricaneTrackGeoJson.propTypes = {
index: PropTypes.number.isRequired,
};
Loading