ASM #15658 - WarnGen: inclusion of watch information in marine products
Change-Id: I9f99e6136027f1a6419800f87e5a5709fe5be6b9 Former-commit-id: 6bfa1d6662b00925814be7126592bbe340651649
This commit is contained in:
parent
aaabfa6cd0
commit
ea9230b37c
9 changed files with 288 additions and 18 deletions
|
@ -0,0 +1,99 @@
|
|||
package com.raytheon.viz.warngen.gis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
import com.raytheon.uf.common.dataplugin.warning.util.WarnFileUtil;
|
||||
import com.raytheon.uf.common.serialization.SingleTypeJAXBManager;
|
||||
import com.raytheon.uf.viz.core.localization.LocalizationManager;
|
||||
import com.raytheon.viz.warngen.gui.WarngenLayer;
|
||||
|
||||
/**
|
||||
* WarngenWordingConfiguration
|
||||
*
|
||||
* <pre>
|
||||
* SOFTWARE HISTORY
|
||||
*
|
||||
* Date Ticket# Engineer Description
|
||||
* ------------ ---------- -------------- --------------------------
|
||||
* 2014-08-28 ASM #15658 D. Friedman Initial Creation.
|
||||
*/
|
||||
@XmlAccessorType(XmlAccessType.NONE)
|
||||
@XmlRootElement(name = "zoneWordingConfig")
|
||||
public class MarineWordingConfiguration {
|
||||
|
||||
private static final String FILE_NAME = "marineZoneWording.xml";
|
||||
|
||||
@XmlElement(name = "entry")
|
||||
private List<MarineWordingEntry> entries = new ArrayList<MarineWordingEntry>();
|
||||
|
||||
public List<MarineWordingEntry> getEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void setEntries(List<MarineWordingEntry> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE)
|
||||
public static class MarineWordingEntry {
|
||||
@XmlAttribute(name = "match")
|
||||
private String matchText;
|
||||
@XmlAttribute(name = "replace")
|
||||
private String replacementText;
|
||||
|
||||
private Pattern ugcPattern;
|
||||
|
||||
public String getMatchText() {
|
||||
return matchText;
|
||||
}
|
||||
|
||||
public void setMatchText(String matchText) {
|
||||
this.matchText = matchText;
|
||||
this.ugcPattern = null;
|
||||
}
|
||||
|
||||
public String getReplacementText() {
|
||||
return replacementText;
|
||||
}
|
||||
|
||||
public void setReplacementText(String replacementText) {
|
||||
this.replacementText = replacementText;
|
||||
}
|
||||
|
||||
public Pattern getUgcPattern() {
|
||||
if (ugcPattern == null) {
|
||||
if (matchText != null) {
|
||||
ugcPattern = Pattern.compile(matchText);
|
||||
}
|
||||
}
|
||||
return ugcPattern;
|
||||
}
|
||||
}
|
||||
|
||||
private static final SingleTypeJAXBManager<MarineWordingConfiguration> jaxb = SingleTypeJAXBManager
|
||||
.createWithoutException(MarineWordingConfiguration.class);
|
||||
|
||||
|
||||
public static MarineWordingConfiguration load(WarngenLayer forLayer) throws Exception {
|
||||
String xmlText = WarnFileUtil.convertFileContentsToString(FILE_NAME,
|
||||
LocalizationManager.getInstance().getCurrentSite(),
|
||||
forLayer.getLocalizedSite());
|
||||
|
||||
MarineWordingConfiguration config = (MarineWordingConfiguration)
|
||||
jaxb.unmarshalFromXml(xmlText);
|
||||
for (MarineWordingEntry entry : config.getEntries()) {
|
||||
// Validate patterns by compiling now.
|
||||
entry.getUgcPattern();
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
|
@ -33,6 +33,7 @@ import java.util.List;
|
|||
* Date Ticket# Engineer Description
|
||||
* ------------ ---------- ----------- --------------------------
|
||||
* Jul 16, 2014 3419 jsanchez Initial creation
|
||||
* Aug 28, 2014 ASM #15658 D. Friedman Add marine zone list.
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
|
@ -58,6 +59,8 @@ public class Watch {
|
|||
|
||||
private List<String> partOfState;
|
||||
|
||||
private List<String> marineAreas;
|
||||
|
||||
public Watch(String state, String action, String phenSig, String etn,
|
||||
Date startTime, Date endTime) {
|
||||
this.state = state;
|
||||
|
@ -132,6 +135,14 @@ public class Watch {
|
|||
this.etn = etn;
|
||||
}
|
||||
|
||||
public List<String> getMarineAreas() {
|
||||
return marineAreas;
|
||||
}
|
||||
|
||||
public void setMarineAreas(List<String> marineAreas) {
|
||||
this.marineAreas = marineAreas;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
|
|
|
@ -54,8 +54,11 @@ import com.raytheon.uf.common.status.IUFStatusHandler;
|
|||
import com.raytheon.uf.common.status.UFStatus;
|
||||
import com.raytheon.uf.common.status.UFStatus.Priority;
|
||||
import com.raytheon.uf.common.time.util.TimeUtil;
|
||||
import com.raytheon.uf.common.util.Pair;
|
||||
import com.raytheon.uf.viz.core.exception.VizException;
|
||||
import com.raytheon.uf.viz.core.requests.ThriftClient;
|
||||
import com.raytheon.viz.core.mode.CAVEMode;
|
||||
import com.raytheon.viz.warngen.gis.MarineWordingConfiguration.MarineWordingEntry;
|
||||
import com.raytheon.viz.warngen.gui.WarngenLayer;
|
||||
import com.raytheon.viz.warngen.gui.WarngenLayer.GeoFeatureType;
|
||||
import com.vividsolutions.jts.geom.Geometry;
|
||||
|
@ -72,6 +75,7 @@ import com.vividsolutions.jts.geom.Polygon;
|
|||
* ------------ ---------- ----------- --------------------------
|
||||
* Jul 17, 2014 3419 jsanchez Initial creation
|
||||
* Aug 20, 2014 ASM #16703 D. Friedman Ensure watches have a state attribute.
|
||||
* Aug 28, 2014 ASM #15658 D. Friedman Add marine zones.
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
|
@ -106,10 +110,16 @@ public class WatchUtil {
|
|||
|
||||
private static final String COUNTY_FE_AREA_FIELD = "FE_AREA";
|
||||
|
||||
private static final Object MARINE_ZONE_UGC_FIELD = "ID";
|
||||
|
||||
private static final Object MARINE_ZONE_NAME_FIELD = "NAME";
|
||||
|
||||
private static final String STATE_FIELD = "STATE";
|
||||
|
||||
private static final String COUNTY_TABLE = "County";
|
||||
|
||||
private static final String MARINE_ZONE_TABLE = "MarineZones";
|
||||
|
||||
private static final String PARENT_NAME_FIELD = "NAME";
|
||||
|
||||
private static final String[] REQUEST_FIELDS = new String[] {
|
||||
|
@ -118,8 +128,12 @@ public class WatchUtil {
|
|||
|
||||
private GeospatialData[] countyGeoData;
|
||||
|
||||
private GeospatialData[] marineGeoData;
|
||||
|
||||
private WarngenLayer warngenLayer;
|
||||
|
||||
private MarineWordingConfiguration marineWordingConfig;
|
||||
|
||||
public WatchUtil(WarngenLayer warngenLayer) throws InstantiationException {
|
||||
countyGeoData = warngenLayer.getGeodataFeatures(COUNTY_TABLE,
|
||||
warngenLayer.getLocalizedSite());
|
||||
|
@ -155,6 +169,17 @@ public class WatchUtil {
|
|||
Validate.isTrue(watchAreaBuffer >= 0,
|
||||
"'includedWatchAreaBuffer' can not be negative in .xml file");
|
||||
|
||||
if (config.isIncludeMarineAreasInWatches()) {
|
||||
marineGeoData = warngenLayer.getGeodataFeatures(MARINE_ZONE_TABLE,
|
||||
warngenLayer.getLocalizedSite());
|
||||
if (marineGeoData == null) {
|
||||
throw new VizException("Cannot get geospatial data for "
|
||||
+ MARINE_ZONE_TABLE + "-based watches");
|
||||
}
|
||||
|
||||
marineWordingConfig = MarineWordingConfiguration.load(warngenLayer);
|
||||
}
|
||||
|
||||
String[] includedWatches = config.getIncludedWatches();
|
||||
|
||||
if ((includedWatches != null) && (includedWatches.length > 0)) {
|
||||
|
@ -174,10 +199,16 @@ public class WatchUtil {
|
|||
entityClass = PracticeActiveTableRecord.class;
|
||||
}
|
||||
|
||||
HashSet<String> allUgcs = new HashSet<String>(
|
||||
warngenLayer.getAllUgcs(GeoFeatureType.COUNTY));
|
||||
Set<String> marineUgcs = null;
|
||||
if (config.isIncludeMarineAreasInWatches()) {
|
||||
marineUgcs = warngenLayer.getAllUgcs(GeoFeatureType.MARINE);
|
||||
allUgcs.addAll(marineUgcs);
|
||||
}
|
||||
|
||||
DbQueryRequest request = buildRequest(simulatedTime,
|
||||
phenSigConstraint.toString(),
|
||||
warngenLayer.getAllUgcs(GeoFeatureType.COUNTY),
|
||||
entityClass);
|
||||
phenSigConstraint.toString(), allUgcs, entityClass);
|
||||
DbQueryResponse response = (DbQueryResponse) ThriftClient
|
||||
.sendRequest(request);
|
||||
|
||||
|
@ -192,9 +223,14 @@ public class WatchUtil {
|
|||
/ KmToDegrees);
|
||||
System.out.println("create watch area buffer time: "
|
||||
+ (System.currentTimeMillis() - t0));
|
||||
Set<String> validUgcZones = warngenLayer
|
||||
.getUgcsForWatches(watchArea, GeoFeatureType.COUNTY);
|
||||
watches = processRecords(records, validUgcZones);
|
||||
HashSet<String> validUgcZones = new HashSet<String>(
|
||||
warngenLayer.getUgcsForWatches(watchArea,
|
||||
GeoFeatureType.COUNTY));
|
||||
if (config.isIncludeMarineAreasInWatches()) {
|
||||
validUgcZones.addAll(warngenLayer.getUgcsForWatches(
|
||||
watchArea, GeoFeatureType.MARINE));
|
||||
}
|
||||
watches = processRecords(records, validUgcZones, marineUgcs);
|
||||
} catch (RuntimeException e) {
|
||||
statusHandler
|
||||
.handle(Priority.ERROR,
|
||||
|
@ -302,12 +338,13 @@ public class WatchUtil {
|
|||
*
|
||||
* @param activeTableRecords
|
||||
* @param validUgcZones
|
||||
* @param marineUgcs
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private List<Watch> processRecords(
|
||||
List<ActiveTableRecord> activeTableRecords,
|
||||
Set<String> validUgcZones) {
|
||||
Set<String> validUgcZones, Set<String> marineUgcs) {
|
||||
List<Watch> watches = new ArrayList<Watch>();
|
||||
|
||||
/*
|
||||
|
@ -329,14 +366,16 @@ public class WatchUtil {
|
|||
* validUgcZones here.
|
||||
*/
|
||||
String ugcZone = ar.getUgcZone();
|
||||
String state = getStateName(ugcZone.substring(0, 2));
|
||||
String state = null;
|
||||
|
||||
/*
|
||||
* Temporary fix for SS DR #16703. Remove when marine watch wording
|
||||
* is fixed.
|
||||
*/
|
||||
if (state == null)
|
||||
continue;
|
||||
if (marineUgcs != null && marineUgcs.contains(ugcZone)) {
|
||||
// Just leave state == null
|
||||
} else {
|
||||
state = getStateName(ugcZone.substring(0, 2));
|
||||
if (state == null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
String action = ar.getAct();
|
||||
String phenSig = ar.getPhensig();
|
||||
|
@ -360,9 +399,13 @@ public class WatchUtil {
|
|||
for (Entry<Watch, List<String>> entry : map.entrySet()) {
|
||||
Watch watch = entry.getKey();
|
||||
watch.setAreas(entry.getValue());
|
||||
List<String> partOfState = new ArrayList<String>(
|
||||
determineAffectedPortions(watch.getAreas()));
|
||||
watch.setPartOfState(partOfState);
|
||||
if (watch.getState() != null) {
|
||||
List<String> partOfState = new ArrayList<String>(
|
||||
determineAffectedPortions(watch.getAreas()));
|
||||
watch.setPartOfState(partOfState);
|
||||
} else {
|
||||
watch.setMarineAreas(determineMarineAreas(watch.getAreas()));
|
||||
}
|
||||
watches.add(watch);
|
||||
}
|
||||
|
||||
|
@ -412,6 +455,40 @@ public class WatchUtil {
|
|||
return affectedPortions;
|
||||
}
|
||||
|
||||
private List<String> determineMarineAreas(List<String> areas) {
|
||||
HashSet<Pair<Integer, String>> groupedAreas = new HashSet<Pair<Integer,String>>();
|
||||
for (String area : areas) {
|
||||
int entryIndex = 0;
|
||||
for (MarineWordingEntry entry : marineWordingConfig.getEntries()) {
|
||||
if (entry.getUgcPattern().matcher(area).matches()) {
|
||||
String replacement = entry.getReplacementText();
|
||||
if (replacement != null) {
|
||||
if (replacement.length() > 0) {
|
||||
groupedAreas.add(new Pair<Integer, String>(
|
||||
entryIndex, entry.getReplacementText()));
|
||||
}
|
||||
} else {
|
||||
groupedAreas.add(new Pair<Integer, String>(entryIndex,
|
||||
getMarineZoneName(area)));
|
||||
}
|
||||
}
|
||||
entryIndex++;
|
||||
}
|
||||
}
|
||||
ArrayList<Pair<Integer, String>> sorted = new ArrayList<Pair<Integer,String>>(groupedAreas);
|
||||
Collections.sort(sorted, new Comparator<Pair<Integer, String>>() {
|
||||
public int compare(Pair<Integer, String> o1, Pair<Integer, String> o2) {
|
||||
int r = o1.getFirst().compareTo(o2.getFirst());
|
||||
return r != 0 ? r : o1.getSecond().compareTo(o2.getSecond());
|
||||
};
|
||||
});
|
||||
ArrayList<String> result = new ArrayList<String>(sorted.size());
|
||||
for (Pair<Integer, String> value : sorted) {
|
||||
result.add(value.getSecond());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full state name from the state abbreviation.
|
||||
*
|
||||
|
@ -446,6 +523,16 @@ public class WatchUtil {
|
|||
return null;
|
||||
}
|
||||
|
||||
private String getMarineZoneName(String ugc) {
|
||||
for (GeospatialData g : marineGeoData) {
|
||||
if (((String) g.attributes.get(MARINE_ZONE_UGC_FIELD))
|
||||
.endsWith(ugc)) {
|
||||
return (String) g.attributes.get(MARINE_ZONE_NAME_FIELD);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Based on AWIPS 1 SELSparagraphs.C SELSparagraphs::processWOU().
|
||||
private String mungeFeAreas(Set<String> feAreas) {
|
||||
String abrev = "";
|
||||
|
|
|
@ -61,6 +61,7 @@ import com.raytheon.uf.common.status.UFStatus.Priority;
|
|||
* Apr 24, 2013 1943 jsanchez Marked areaConfig as Deprecated.
|
||||
* Oct 22, 2013 2361 njensen Removed ISerializableObject
|
||||
* Apr 28, 2014 3033 jsanchez Properly handled back up configuration (*.xml) files.
|
||||
* Aug 28, 2014 ASM #15658 D. Friedman Add marine zone watch wording option.
|
||||
* </pre>
|
||||
*
|
||||
* @author chammack
|
||||
|
@ -103,6 +104,9 @@ public class WarngenConfiguration {
|
|||
@XmlElement(name = "includedWatch")
|
||||
private String[] includedWatches;
|
||||
|
||||
@XmlElement
|
||||
private boolean includeMarineAreasInWatches;
|
||||
|
||||
@XmlElementWrapper(name = "durations")
|
||||
@XmlElement(name = "duration")
|
||||
private int[] durations;
|
||||
|
@ -392,6 +396,14 @@ public class WarngenConfiguration {
|
|||
return includedWatches;
|
||||
}
|
||||
|
||||
public boolean isIncludeMarineAreasInWatches() {
|
||||
return includeMarineAreasInWatches;
|
||||
}
|
||||
|
||||
public void setIncludeMarineAreasInWatches(boolean includeMarineAreasInWatches) {
|
||||
this.includeMarineAreasInWatches = includeMarineAreasInWatches;
|
||||
}
|
||||
|
||||
public boolean getEnableRestart() {
|
||||
return enableRestart;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
##### Evan Bookbinder 05-05-2013 handleClosesPoints and 3rd bullet changes (OVER & now)
|
||||
##### Evan Bookbinder 09-20-2013 Fixed rural area otherPoints in pathcast section, added rural phrase
|
||||
##### Qinglu Lin 03-17-2014 DR 16309. Updated inserttorwatches and insertsvrwatches.
|
||||
##### Qinglu Lin 05-21-2014 DR 16309. Updated inserttorwatches and insertsvrwatches by changing 'FOR##' to 'FOR ##'.
|
||||
##### Qinglu Lin 05-21-2014 DR 16309. Updated inserttorwatches and insertsvrwatches by changing 'FOR##' to 'FOR ##'.
|
||||
##### D. Friedman 08-28-2014 ASM #15658. Add marine watch wording.
|
||||
####################################################################################################
|
||||
#*
|
||||
Mile Marker Test Code
|
||||
|
@ -218,7 +219,11 @@ ${dateUtil.period(${tornadoWatch.endTime},${timeFormat.plain}, 15, ${localtimezo
|
|||
#set($count = 0)
|
||||
#foreach(${watch} in ${tornadoWatches})
|
||||
#set($count = $count + 1)
|
||||
#if(!${watch.marineAreas})
|
||||
#areaFormat(${watch.partOfState} true false true)${watch.state}##
|
||||
#else
|
||||
#formatMarineAreas(${watch.marineAreas})
|
||||
#end
|
||||
#if($count == $numPortions - 1)
|
||||
AND ##
|
||||
#elseif($count < $numPortions)
|
||||
|
@ -263,7 +268,11 @@ ${dateUtil.period(${svrWatch.endTime},${timeFormat.plain}, 15, ${localtimezone})
|
|||
#set($count = 0)
|
||||
#foreach(${watch} in ${severeWatches})
|
||||
#set($count = $count + 1)
|
||||
#if(!${watch.marineAreas})
|
||||
#areaFormat(${watch.partOfState} true false true)${watch.state}##
|
||||
#else
|
||||
#formatMarineAreas(${watch.marineAreas})
|
||||
#end
|
||||
#if($count == $numPortions - 1)
|
||||
AND ##
|
||||
#elseif($count < $numPortions)
|
||||
|
@ -278,6 +287,25 @@ ${dateUtil.period(${svrWatch.endTime},${timeFormat.plain}, 15, ${localtimezone})
|
|||
#end
|
||||
########END
|
||||
|
||||
#macro(formatMarineAreas $marineAreas)
|
||||
#set($macount = 0)
|
||||
#set($numMarineAreas = ${list.size(${marineAreas})})
|
||||
#foreach(${marineArea} in ${marineAreas})
|
||||
#set($macount = $macount + 1)
|
||||
#if(${marineArea}=="THE ADJACENT COASTAL WATERS" && $macount > 1)
|
||||
OTHER ADJACENT COASTAL WATERS##
|
||||
#else
|
||||
${marineArea}##
|
||||
#end
|
||||
#if($macount == $numMarineAreas - 1)
|
||||
AND ##
|
||||
#elseif($macount < $numMarineAreas)
|
||||
...##
|
||||
#end
|
||||
#end
|
||||
#end
|
||||
########END MACRO
|
||||
|
||||
#macro(printcoords $coordinates $list)
|
||||
#set($count = 0)
|
||||
LAT...LON ##
|
||||
|
|
|
@ -58,6 +58,9 @@ turned on unless the corresponding .vm file is turned on in a given template's .
|
|||
<includedWatch>SV.A</includedWatch>
|
||||
</includedWatches>
|
||||
|
||||
<!-- Include references to marine zones for watches. See marineZoneWording.xml -->
|
||||
<includeMarineAreasInWatches>true</includeMarineAreasInWatches>
|
||||
|
||||
<!-- durations: the list of possible durations -->
|
||||
<defaultDuration>60</defaultDuration>
|
||||
<durations>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<!--
|
||||
This file defines grouping and replacement text for marine zones in the watch
|
||||
wording of the template.
|
||||
|
||||
For each entry, the "match" attribute defines a regular expression that is
|
||||
used to match marine zone UGCs. The "replace" attribute defines the text to
|
||||
pass to the template. If the replacement text is empty, the marine zone
|
||||
will not be used. If the "replace" attribute is not present, the marine
|
||||
zone's original name is used.
|
||||
|
||||
Marine zone UGCs are matched against entries in their given order; the first
|
||||
match is used. The order of the entries also determines the order of the
|
||||
values passed to the template.
|
||||
-->
|
||||
<zoneWordingConfig>
|
||||
<entry match="^LEZ.*" replace="THE ADJACENT COASTAL WATERS OF LAKE ERIE" />
|
||||
<entry match="^LHZ.*" replace="THE ADJACENT COASTAL WATERS OF LAKE HURON" />
|
||||
<entry match="^LMZ.*" replace="THE ADJACENT COASTAL WATERS OF LAKE MICHIGAN" />
|
||||
<entry match="^LOZ.*" replace="THE ADJACENT COASTAL WATERS OF LAKE ONTARIO" />
|
||||
<entry match="^LSZ.*" replace="THE ADJACENT COASTAL WATERS OF LAKE SUPERIOR" />
|
||||
<entry match="^LCZ.*" replace="THE ADJACENT COASTAL WATERS OF LAKE SAINT CLAIRE" />
|
||||
<entry match="^SLZ.*" replace="" /> <!-- Saint Lawrence River -->
|
||||
<entry match="^.*" replace="THE ADJACENT COASTAL WATERS" />
|
||||
</zoneWordingConfig>
|
|
@ -61,6 +61,9 @@ turned on unless the corresponding .vm file is turned on in a given template's .
|
|||
<includedWatch>SV.A</includedWatch>
|
||||
</includedWatches>
|
||||
|
||||
<!-- Include references to marine zones for watches. See marineZoneWording.xml. -->
|
||||
<includeMarineAreasInWatches>true</includeMarineAreasInWatches>
|
||||
|
||||
<!-- durations: the list of possible durations of the warning -->
|
||||
<defaultDuration>30</defaultDuration>
|
||||
<durations>
|
||||
|
|
|
@ -63,6 +63,9 @@ turned on unless the corresponding .vm file is turned on in a given template's .
|
|||
<includedWatch>SV.A</includedWatch>
|
||||
</includedWatches>
|
||||
|
||||
<!-- Include references to marine zones for watches. See marineZoneWording.xml. -->
|
||||
<includeMarineAreasInWatches>true</includeMarineAreasInWatches>
|
||||
|
||||
<!-- durations: the list of possible durations of the svs -->
|
||||
<!-- THIS REALLY SERVES NO PURPOSE BUT WILL CRASH WARNGEN IF REMVOED -->
|
||||
<defaultDuration>30</defaultDuration>
|
||||
|
|
Loading…
Add table
Reference in a new issue