diff --git a/cht_cyclones/__init__.py b/cht_cyclones/__init__.py index 829d424..70bce39 100644 --- a/cht_cyclones/__init__.py +++ b/cht_cyclones/__init__.py @@ -9,3 +9,5 @@ from .tropical_cyclone import TropicalCyclone from .track_database import CycloneTrackDatabase +from .track_selector import track_selector + diff --git a/cht_cyclones/cyclone_track_selector.yml b/cht_cyclones/cyclone_track_selector.yml new file mode 100644 index 0000000..d1bd6ac --- /dev/null +++ b/cht_cyclones/cyclone_track_selector.yml @@ -0,0 +1,42 @@ +window: + title: Select Cyclone Track ... + width: 800 + height: 500 + module: cht_cyclones.track_selector + variable_group: cyclone_track_selector + modal: true + cancel: true +element: +- style: edit + variable: distance + method: edit_filter + text: Distance (km) + position: + x: 100 + y: 70 + width: 50 + height: 20 +- style: edit + variable: year0 + method: edit_filter + text: Year + position: + x: 100 + y: 45 + width: 50 + height: 20 +- style: edit + variable: year1 + method: edit_filter + position: + x: 160 + y: 45 + width: 50 + height: 20 +- style: mapbox + id: track_selector_map + position: + x: 20 + y: 100 + width: -20 + height: -20 diff --git a/cht_cyclones/ensemble.py b/cht_cyclones/ensemble.py index ab72ea4..44c2258 100644 --- a/cht_cyclones/ensemble.py +++ b/cht_cyclones/ensemble.py @@ -25,9 +25,11 @@ class TropicalCycloneEnsemble: def __init__(self, tropical_cyclone, name="ensemble", number_of_realizations=10, + compute_wind_fields=True, dt=3, tstart=None, tend=None, + duration=None, track_path=None, spw_path=None, mean_abs_cte24=19.0397, @@ -44,6 +46,8 @@ def __init__(self, tropical_cyclone, # If name is not given, use the name of the tropical cyclone self.name = name + self.compute_wind_fields = compute_wind_fields + if track_path is None: track_path = os.getcwd() if spw_path is None: @@ -71,7 +75,7 @@ def __init__(self, tropical_cyclone, self.sc_ve = sc_ve # auto-regression VE = 1 = no auto-regression self.bias_ve = bias_ve # bias per hour - self.tropical_cyclone = tropical_cyclone + self.tropical_cyclone = copy.deepcopy(tropical_cyclone) # Set scale factor of the best track to 1.0 self.tropical_cyclone.track.gdf.loc[:,"wind_scale_factor"] = pd.Series(np.zeros(len(self.tropical_cyclone.track.gdf)) + 1.0, @@ -85,7 +89,12 @@ def __init__(self, tropical_cyclone, self.tstart = tstart if tend is not None: - self.tropical_cyclone.track.shorten(tend=tend) + # Do we really want to shorten the track? + self.tropical_cyclone.track.shorten(tend=tend) + else: + if duration is not None: + tend = self.tstart + pd.Timedelta(duration, unit="h") + self.tropical_cyclone.track.shorten(tend=tend) # Make sure the metric track is computed self.tropical_cyclone.compute_metric_track() @@ -111,7 +120,8 @@ def generate(self): self.generate_tracks() # Generates the tracks and vmax values for the individual ensemble members - self.compute_wind_fields() # Computes the wind fields by scaling + if self.compute_wind_fields: + self.compute_wind_fields() # Computes the wind fields by scaling def generate_tracks(self): @@ -204,7 +214,7 @@ def to_gdf(self, # Get the GDF if option == "tracks": gdf = self.tracks_to_gdf() - elif option == "outline": + elif option == "outline" or option == "cone": gdf = self.outline_to_gdf(buffer, only_forecast) # Write to file diff --git a/cht_cyclones/fileio.py b/cht_cyclones/fileio.py index ddcc3c5..88c1e95 100644 --- a/cht_cyclones/fileio.py +++ b/cht_cyclones/fileio.py @@ -1,3 +1,7 @@ +from datetime import datetime +import geopandas as gpd +from shapely.geometry import Point + class TropicalCycloneTrack: def __init__(self): pass diff --git a/cht_cyclones/track_database.py b/cht_cyclones/track_database.py index 3a3aa94..53cadd7 100644 --- a/cht_cyclones/track_database.py +++ b/cht_cyclones/track_database.py @@ -105,6 +105,8 @@ def get_track(self, index): # Create a TropicalCyclone object tc = TropicalCyclone(name=self.name[index]) + gdf = gpd.GeoDataFrame() + # Add track for it in range(self.ntimes): # Check if track is finite @@ -159,42 +161,41 @@ def get_track(self, index): R100_NW = -999.0 # Create a geopandas dataframe - gdf = gpd.GeoDataFrame( + gdf_point = gpd.GeoDataFrame( { "datetime": [tc_time_string], "geometry": [point], "vmax": [vmax], "pc": [pc], - "RMW": [RMW], - "R35_NE": [R35_NE], - "R35_SE": [R35_SE], - "R35_SW": [R35_SW], - "R35_NW": [R35_NW], - "R50_NE": [R50_NE], - "R50_SE": [R50_SE], - "R50_SW": [R50_SW], - "R50_NW": [R50_NW], - "R65_NE": [R65_NE], - "R65_SE": [R65_SE], - "R65_SW": [R65_SW], - "R65_NW": [R65_NW], - "R100_NE": [R100_NE], - "R100_SE": [R100_SE], - "R100_SW": [R100_SW], - "R100_NW": [R100_NW], + "rmw": [RMW], + "r35_ne": [R35_NE], + "r35_se": [R35_SE], + "r35_sw": [R35_SW], + "r35_nw": [R35_NW], + "r50_ne": [R50_NE], + "r50_se": [R50_SE], + "r50_sw": [R50_SW], + "r50_nw": [R50_NW], + "r65_ne": [R65_NE], + "r65_se": [R65_SE], + "r65_sw": [R65_SW], + "r65_nw": [R65_NW], + "r100_ne": [R100_NE], + "r100_se": [R100_SE], + "r100_sw": [R100_SW], + "r100_nw": [R100_NW], } ) - # Set CRS coordinate system - gdf.set_crs(epsg=4326, inplace=True) - # Append self - tc.track = pd.concat([tc.track, gdf]) + gdf = pd.concat([gdf, gdf_point]) + + # Replace -999.0 with NaN + gdf = gdf.replace(-999.0, np.nan) + gdf = gdf.reset_index(drop=True) + gdf = gdf.set_crs(crs=4326, inplace=True) - # Done with this - tc.track = tc.track.reset_index(drop=True) - tc.track = tc.track.drop([0]) # remove the dummy - tc.track = tc.track.reset_index(drop=True) + tc.track.gdf = gdf return tc diff --git a/cht_cyclones/track_selector.py b/cht_cyclones/track_selector.py new file mode 100644 index 0000000..8cd6c3b --- /dev/null +++ b/cht_cyclones/track_selector.py @@ -0,0 +1,89 @@ +import os + +def track_selector(database, app, lon=0.0, lat=0.0, distance=1000.0, year_min=1850, year_max=2030): + + app.gui.setvar("cyclone_track_selector", "lon", lon) + app.gui.setvar("cyclone_track_selector", "lat", lat) + app.gui.setvar("cyclone_track_selector", "distance", distance) + app.gui.setvar("cyclone_track_selector", "year0", year_min) + app.gui.setvar("cyclone_track_selector", "year1", year_max) + app.gui.setvar("cyclone_track_selector", "name", "") + + data = {} + data["track_database"] = database + + # Read GUI config file + config_file = os.path.join(os.path.dirname(__file__), "cyclone_track_selector.yml") + okay, data = app.gui.popup(config_file, id="track_selector", data=data) + + track = None + if okay: + # Get the track from the database + track = database.get_track(data["database_index"]) + + return track, okay + +def map_ready(widget): + + print("Selector map is ready !") + + gui = widget.element.gui + + mp = gui.popup_window["track_selector"].find_element_by_id("track_selector_map").widget + mp.jump_to(0.0, 0.0, 1) + data = gui.popup_data + # Container layers + data["track_selector"]["main_layer"] = mp.add_layer("track_selector") + # Tracks layers + data["track_selector"]["track_layer"] = data["track_selector"]["main_layer"].add_layer( + "tracks", + type="line_selector", + file_name="tracks.geojson", + select=select_track, + selection_type="single", + line_color="dodgerblue", + line_width=2, + line_color_selected="red", + line_width_selected=3, + hover_param="description", + ) + + # Update data in tracks layer + update_tracks(gui) + + +def update_tracks(gui): + + data = gui.popup_data + + tdb = data["track_selector"]["track_database"] + tracks_layer = data["track_selector"]["track_layer"] + + # Get filter data + distance = gui.getvar("cyclone_track_selector", "distance") + year_min = gui.getvar("cyclone_track_selector", "year0") + year_max = gui.getvar("cyclone_track_selector", "year1") + lon = gui.getvar("cyclone_track_selector", "lon") + lat = gui.getvar("cyclone_track_selector", "lat") + + # Get indices based on filter + index = tdb.filter( + lon=lon, lat=lat, distance=distance, year_min=year_min, year_max=year_max + ) + + # Get GeoDataFrame of tracks + gdf = tdb.to_gdf(index=index) + + tracks_layer.set_data(gdf, 0) + + +def map_moved(coords, widget): + pass + + +def select_track(feature, widget): + widget.element.gui.popup_data["track_selector"]["database_index"] = feature["properties"]["database_index"] + + +def edit_filter(val, widget): + update_tracks(widget.element.gui)