import proxies from "./../../data/datasources/proxy.js";
import { retrieve } from "./../../data/data.js";
import {
Layers,
renderMap,
recenter,
addCustomLegend,
} from "./../../map/map.js";
/**
* This class contains methods which can be used to model various flood damage and mitigation scenarios for hydrological analyses.
* @class
* @name floodDM
*/
export default class floodDM {
city = "";
depth = 0;
mapRendered = false;
selectedfeature = null;
/**
* Initialize the damage scenario for a particular city, 100 or 500 year flood scenario and render based on google or leaflet
* @method initDamageScenario
* @memberof floodDM
* @instance
* @async
* @param {Object} params - Parameters to define map data to initialize
* @param {String} params.maptype - Type of map to initialize as a String, currently supported values are google or leaflet
* @param {String} params.key - Required if maptype is google, pass in the API key for google maps
* @param {Object} args - Arguments to initialize flood damage scenario being intialized
* @param {String} args.city - City name as a String, currently supported cities include Bettendorf, Cedar Falls, Cedar Rapids, Davenport, Iowa City, Waterloo and Waverly
* @param {number} args.depth - Number value indicating flood depth
* @returns {Promise<Object>} A Promise that resolves with an object containing the scenario results, and damage values for the scenario
* @example
* scenario = new floodDM()
* scenario.initDamageScenario({params:{maptype:'leaflet'},args:{city:"Cedar Falls", depth:18, scenario:'500-year'}})
*/
async initDamageScenario({ params, args, data } = {}) {
// Destructure the arguments passed to the function
let { city, depth, scenario } = args;
this.city = city;
this.depth = depth;
// Set default map type as leaflet
if (params === undefined) {
this.maptype = "leaflet";
params = {};
} else if (params.maptype === undefined) {
this.maptype = "leaflet";
} else {
this.maptype = params.maptype;
}
// Check if key is present if google map is rendered
if (this.maptype === "google") {
if (params.key === undefined) {
console.log("param.key value is required to render a google map");
return;
}
}
// Normalize the scenario argument to a specific format
if (scenario === "100-year") scenario = "x100_year";
else if (scenario === "500-year") scenario = "x500_year";
else {
console.log("Pass 100-year or 500-year flood scenario in the args.scenario");
return;
}
// Set class member as 100 or 500 year scenario
this.scenario = scenario;
// Retrieve the flood layer based on the selected scenario
let flood_layer = await retrieve({
params: {
source: "flooddamage_dt",
datatype: scenario,
transform: "eval",
},
args: { sourceType: this.city },
});
// Retrieve the utilities layer based on the selected scenario
let utilities_layer;
// Utilities Layer data source is avilable for Cedar Falls only
if (this.city === "Cedar Falls") {
utilities_layer = await retrieve({
params: {
source: "flooddamage_dt",
datatype: "utilities",
transform: "eval",
},
args: { sourceType: this.city },
});
}
// Retrieve the vehicles layer based on the selected scenario
let vehicles_layer = await retrieve({
params: {
source: "flooddamage_dt",
datatype: "vehicles",
transform: "eval",
},
args: { sourceType: this.city },
});
// Retrieve the buildings layer based on the selected scenario
let buildings_layer = await retrieve({
params: {
source: "flooddamage_dt",
datatype: "buildings",
transform: "eval",
},
args: { sourceType: this.city },
});
// Retrieve the bridge layer based on the selected scenario
let bridges_layer;
// Bridge Layer data source only exists for Cedar Falls, Cedar Rapids, Davenport, Waverly
if (
this.city === "Cedar Falls" ||
this.city === "Cedar Rapids" ||
this.city === "Davenport" ||
this.city === "Waverly"
)
bridges_layer = await retrieve({
params: {
source: "flooddamage_dt",
datatype: "bridges",
transform: "eval",
},
args: { sourceType: this.city },
});
// Calculate the centroid of the buildings layer so render map center
let centroid = floodDM.featureCollectionCentroid(buildings_layer);
// Render the map if it hasn't been rendered before
if (!this.mapRendered) {
await renderMap({
params: { maptype: this.maptype, lat: centroid[1], lon: centroid[0] },
args: { key: params.key },
});
this.mapRendered = true;
}
// Variable to store and render scenario rendering results
let scenario_results = {
Buildings: {
Structure: 0,
Content: 0,
Income: 0,
Wage: 0,
"Relocation Expenses": 0,
"Rental Income": 0,
"Loss of Life (Day)": 0,
"Loss of Life (Night)": 0,
Debris: 0,
},
Vehicles: {
Day: 0,
Night: 0,
},
Bridges: {
Damage: 0,
},
Utilities: {
Damage: 0,
},
};
/*
* Begin Rendering Vehicles Layer
*/
// Variable to store vehicle layer calculations
let vehicleFeatureCalculations = {};
// Retrieve the vehicle damage function data from damage datasource
let vehicleDamFun = await floodDM.getVehicleDamageFunction();
// Render the vehicle damage layer
await Layers({
args: {
type: "geojson",
name: "Vehicles Layer",
// Set feature style using feature properties
styleFunction: (feature) => {
// Destructure properties according to features based on maptype
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
// Calculate vehicle damage based on flood depth and area
let depth100yr = properties.flood100_avg;
let depth500yr = properties.flood500_avg;
let areaPrc100yr = properties.area_fe_100yr;
let areaPrc500yr = properties.area_fe_500yr;
let car = "PassengerCar";
let light = "LightTruck";
let heavy = "HeavyTruck";
let inundationDepth,
areaPrc,
vehDamPrcntCar,
vehDamPrcntLight,
vehDamPrcntHeavy;
if (this.scenario === "x100_year") {
inundationDepth = depth100yr;
areaPrc = areaPrc100yr;
} else if (this.scenario === "x500_year") {
inundationDepth = depth500yr;
areaPrc = areaPrc500yr;
}
for (let i in vehicleDamFun[car]) {
if (vehicleDamFun[car][i]["floodDepth"] == inundationDepth) {
vehDamPrcntCar = vehicleDamFun[car][i]["damage"];
}
}
for (let i in vehicleDamFun[light]) {
if (vehicleDamFun[light][i]["floodDepth"] == inundationDepth) {
vehDamPrcntLight = vehicleDamFun[light][i]["damage"];
}
}
for (let i in vehicleDamFun[heavy]) {
if (vehicleDamFun[heavy][i]["floodDepth"] == inundationDepth) {
vehDamPrcntHeavy = vehicleDamFun[heavy][i]["damage"];
}
}
// Calculate vehicle damage for different vehicle types at day and night
let carDamageD = parseInt(
(vehDamPrcntCar / 100) * areaPrc * properties.car_val_day
).toLocaleString();
let lightDamageD = parseInt(
(vehDamPrcntLight / 100) * areaPrc * properties.light_val_day
).toLocaleString();
let heavyDamageD = parseInt(
(vehDamPrcntHeavy / 100) * areaPrc * properties.heavy_val_day
).toLocaleString();
let carDamageN = parseInt(
(vehDamPrcntCar / 100) * areaPrc * properties.car_val_night
).toLocaleString();
let lightDamageN = parseInt(
(vehDamPrcntLight / 100) * areaPrc * properties.light_val_night
).toLocaleString();
let heavyDamageN = parseInt(
(vehDamPrcntHeavy / 100) * areaPrc * properties.heavy_val_night
).toLocaleString();
let carDamageDC =
(vehDamPrcntCar / 100) * areaPrc * properties.car_val_day;
let lightDamageDC =
(vehDamPrcntLight / 100) * areaPrc * properties.light_val_day;
let heavyDamageDC =
(vehDamPrcntHeavy / 100) * areaPrc * properties.heavy_val_day;
let carDamageNC =
(vehDamPrcntCar / 100) * areaPrc * properties.car_val_night;
let lightDamageNC =
(vehDamPrcntLight / 100) * areaPrc * properties.light_val_night;
let heavyDamageNC =
(vehDamPrcntHeavy / 100) * areaPrc * properties.heavy_val_night;
let totalDamageD = carDamageDC + lightDamageDC + heavyDamageDC;
let totalDamageN = carDamageNC + lightDamageNC + heavyDamageNC;
// Update scenario variable
if (!isNaN(totalDamageD))
scenario_results["Vehicles"]["Day"] += totalDamageD;
if (!isNaN(totalDamageN))
scenario_results["Vehicles"]["Night"] += totalDamageN;
// Update vehicle damage calculations for each feature
vehicleFeatureCalculations[properties.CensusBloc] = {
"Block Information": properties.CensusBloc,
"Flood Scenario:": this.scenario,
"Avg. Flood Depth:": inundationDepth,
"Car Count (Day):": properties.cars_day,
"Car Count (Night):": properties.cars_night,
"Light Truck Count (Day)": properties.light_day,
"Light Truck Count (Night)": properties.light_night,
"Heavy Truck Count (Day)": properties.heavy_day,
"Heavy Truck Count (Night)": properties.heavy_night,
"Cars Damage (Day)": carDamageD,
"Cars Damage (Night)": carDamageN,
"Light Trucks (Day)": lightDamageD,
"Light Trucks (Night)": lightDamageN,
"Heavy Trucks (Day)": heavyDamageD,
"Heavy Truck (Night)": heavyDamageN,
};
// return style values for the feature
return {
fillColor:
totalDamageD > 1000000
? "#323232"
: totalDamageD > 100000
? "#7E7E7E"
: "#BFBFBF",
weight: 2,
opacity: 1,
color: "black",
dashArray: "3",
fillOpacity: 0.9,
};
},
// Set popup values based on feature properties
popUpFunction: (feature) => {
// Destructure properties according to features based on maptype
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
// return popup values for this feature
return JSON.stringify(
vehicleFeatureCalculations[properties.CensusBloc]
).replaceAll(",", "<br>").replaceAll(/|{|}/g,"");
},
},
data: vehicles_layer,
});
/*
* Begin Rendering Flood Inundation Layer
*/
// Render flood inundation layer
await Layers({
args: {
type: "geojson",
name: "Flood Inundation Layer",
// Set style based on feature properties
styleFunction: (feature) => {
// Return style options for this feature
return {
color: "blue",
weight: 1,
opacity: 1,
fillColor: "blue",
fillOpacity: 0.5,
};
},
},
data: flood_layer,
});
/*
* Begin Rendering Vehicles Layer
*/
// Variable to store building layer calculations
let buildingFeatureCalculations = [];
// Get building, life and debris weight functions
let buildingDamage = await floodDM.getPropertyDamageFunction();
let lifeLoss = await floodDM.getLifeLossFunction();
let debrisLoss = await floodDM.getDebrisWeightFunction();
// Render Building Layer Calculations
await Layers({
args: {
type: "geojson",
name: "Properties Layer",
// Set style based on feature properties
styleFunction: (feature) => {
// Destructure properties according to features based on maptype
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
// Calculate metrics for property damage
let occupancy = properties.occupancy;
let inundationDepth =
scenario === "x100_year"
? properties.depth100
: properties.depth500;
let propertyInputs = buildingDamage[occupancy].find(
(item) => item.floodDepth === inundationDepth
);
let structureDamPercent = propertyInputs["structure"];
let contentDamPercent = propertyInputs["content"];
let incomeRecaptureFactor = propertyInputs["recapturFactor"];
let incomePerDay = propertyInputs["incomePerSqftPerDay"];
let lossFunction = propertyInputs["maxTime"];
let wageRecaptureFactor = propertyInputs["recapturFactor"];
let wagePerDay = propertyInputs["wagePerSqftPerDay"];
let percentOwnerOccupied = propertyInputs["percentOwnerOccupied"];
let rentalCostPerSqftPerDay =
propertyInputs["rentalCostPerSqftPerDay"];
let disruptionCostPerSqft = propertyInputs["disruptionCostPerSqft"];
let relocation, rentalIncome;
if (propertyInputs["structure"] > 10) {
relocation = [
(1 - percentOwnerOccupied / 100) * disruptionCostPerSqft +
(percentOwnerOccupied / 100) *
(disruptionCostPerSqft +
rentalCostPerSqftPerDay * lossFunction * 30),
];
rentalIncome =
(1 - percentOwnerOccupied / 100) *
rentalCostPerSqftPerDay *
lossFunction *
30;
} else {
relocation = 0;
rentalIncome = 0;
}
let strDamage = (properties.val_struct * structureDamPercent) / 100;
let contDamage = (properties.val_cont * contentDamPercent) / 100;
let incomeLoss =
(1 - incomeRecaptureFactor) *
properties.sqfoot *
incomePerDay *
lossFunction *
30;
let wageLoss =
(1 - wageRecaptureFactor) *
properties.sqfoot *
wagePerDay *
lossFunction *
30;
let relocationExpenses = properties.sqfoot * relocation;
let rentalIncomeLoss = properties.sqfoot * rentalIncome;
let totalDamageBuild =
strDamage +
contDamage +
incomeLoss +
wageLoss +
relocationExpenses +
rentalIncomeLoss;
// Update scenario variable with building damage
if (!isNaN(strDamage))
scenario_results.Buildings.Structure += strDamage;
if (!isNaN(contDamage))
scenario_results.Buildings.Content += contDamage;
if (!isNaN(incomeLoss))
scenario_results.Buildings.Income += incomeLoss;
if (!isNaN(wageLoss)) scenario_results.Buildings.Wage += wageLoss;
if (!isNaN(relocationExpenses))
scenario_results.Buildings["Relocation Expenses"] +=
relocationExpenses;
if (!isNaN(rentalIncomeLoss))
scenario_results.Buildings["Rental Income"] += rentalIncomeLoss;
// Calculate metrics for life damage
// Filter life function data using inputs
let lifeInputsOver65 = lifeLoss[occupancy].find((item) => {
return (
item.floodDepth === inundationDepth && item.age === "over 65"
);
});
let lifeInputsUnder65 = lifeLoss[occupancy].find((item) => {
return (
item.floodDepth === inundationDepth && item.age === "under 65"
);
});
let lifeLossDay =
lifeInputsOver65["zone"] * properties.POP_O65DAY +
lifeInputsUnder65["zone"] * properties.POP_U65DAY;
let lifeLossNight =
lifeInputsOver65["zone"] * properties.POP_O65NGT +
lifeInputsUnder65["zone"] * properties.POP_U65NGT;
// Update scenario variable with life damage
if (!isNaN(lifeLossDay))
scenario_results.Buildings["Loss of Life (Day)"] += lifeLossDay;
if (!isNaN(lifeLossNight))
scenario_results.Buildings["Loss of Life (Night)"] += lifeLossNight;
// Calculate metrics for debris damage
// Filter debris function data using inputs
let debrisValue = debrisLoss[occupancy].find((item) => {
return (
item["floodDepth"] == inundationDepth &&
item["foundationType"] == "footing"
);
});
let finishes = debrisValue["finishes"];
let structure = debrisValue["structure"];
let foundation = debrisValue["foundation"];
let debrisWeight = finishes + structure + foundation;
let debrisAmount = (properties.sqfoot * debrisWeight) / 1000;
// Update scenario variable with debris damage
if (!isNaN(debrisAmount))
scenario_results.Buildings.Debris += debrisAmount;
// Store building feature calculations for reuse in popup function
buildingFeatureCalculations.push({
occupancy,
gid: properties.gid,
totalDamageBuild,
strDamage,
contDamage,
incomeLoss,
wageLoss,
relocationExpenses,
rentalIncomeLoss,
debrisAmount,
lifeLossDay,
lifeLossNight,
inundationDepth,
});
// Hide building marker if it is not underwater under the current scenario
if (scenario === "x100_year") {
if (properties.depth100 < 1)
return { display: "none", color: "none", fillColor: "none" };
} else {
if (properties.depth500 < 1)
return { display: "none", color: "none", fillColor: "none" };
}
// Return styling of the building marker later
return {
fillColor:
totalDamageBuild > 1000000
? "red"
: totalDamageBuild > 100000
? "yellow"
: "green",
weight: 2,
opacity: 1,
color: "black",
fillOpacity: 1,
radius: 4,
scale: 4,
strokeWeight: 0.7,
};
},
// Return the popup value based on feature properties
popUpFunction: (feature) => {
// Destructure properties according to features based on maptype
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
// Return value to rendered in the popup of the building
let val = JSON.stringify(
buildingFeatureCalculations.find(
(item) => item.gid === properties.gid
)
);
return val.replaceAll(",", "<br>").replaceAll(/|{|}/g,"");
},
},
data: buildings_layer,
});
/*
* Begin Rendering Vehicle Layer
*/
// Bridges Layer only exists for Cedar Falls, Cedar Rapids, Davenport and Waverly
if (
this.city === "Cedar Falls" ||
this.city === "Cedar Rapids" ||
this.city === "Davenport" ||
this.city === "Waverly"
) {
// Get bridge damage functions data
let bridgeDamFun = await floodDM.getBridgeDamageFunction();
await Layers({
args: {
type: "geojson",
name: "Bridge Layer",
// Set bridge style function
styleFunction: (feature) => {
// Destructure properties according to features based on maptype
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
// Calculate bridge damage
let floodScenario, bridgDam, inundationDepth, bridgeDamPrcnt;
let bridgeT = properties.bridgeT;
let scour = String(properties.ScourIndex);
let bridgeCost = properties.Cost;
let depth100 = properties.flood100;
let depth500 = properties.flood500;
let depth100yr = "100 yr";
let depth500yr = "500 yr";
if (scenario === "x100_year") {
floodScenario = "100-year";
} else {
floodScenario = "500-year";
}
if (floodScenario === "100-yr" && depth100 === "yes") {
inundationDepth = depth100yr;
} else if (floodScenario === "500-yr") {
inundationDepth = depth500yr;
} else {
bridgDam = 0;
}
for (let i in bridgeDamFun[bridgeT]) {
if (
bridgeDamFun[bridgeT][i]["ScourPotential"] == scour &&
bridgeDamFun[bridgeT][i]["Flood Scenario"] == inundationDepth
) {
bridgeDamPrcnt = bridgeDamFun[bridgeT][i]["damage prcnt"];
}
}
bridgDam = bridgeDamPrcnt * bridgeCost * 1000;
// Update scenario variable
if (!isNaN(bridgDam))
scenario_results["Bridges"]["Damage"] += bridgDam;
// Return styling
return {
scale: 5,
radius: 2,
weight: 10,
fillColor: "black",
color: "black",
fillOpacity: 0.5,
strokeWeight: 0.7,
};
},
popUpFunction: (feature) => {
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
console.log(feature);
let totalBridgDam = 0,
floodScenario,
bridgDam,
inundationDepth,
bridgeDamPrcnt,
bridgeFunction;
let bridgeT = properties.bridgeT;
let scour = String(properties.ScourIndex);
let bridgeCost = properties.Cost;
let depth100 = properties.flood100;
let depth500 = properties.flood500;
let depth100yr = "100 yr";
let depth500yr = "500 yr";
if (scenario === "x100_year") {
floodScenario = "100-year";
} else {
floodScenario = "500-year";
}
if (floodScenario === "100-yr" && depth100 === "yes") {
inundationDepth = depth100yr;
} else if (floodScenario === "500-yr") {
inundationDepth = depth500yr;
} else {
bridgDam = 0;
}
for (let i in bridgeDamFun[bridgeT]) {
if (
bridgeDamFun[bridgeT][i]["ScourPotential"] == scour &&
bridgeDamFun[bridgeT][i]["Flood Scenario"] == inundationDepth
) {
bridgeDamPrcnt = bridgeDamFun[bridgeT][i]["damage prcnt"];
bridgeFunction = bridgeDamFun[bridgeT][i]["functional"];
}
}
bridgDam = bridgeDamPrcnt * bridgeCost * 1000;
let val = JSON.stringify({
Bridge_ID: properties.HighwayBri,
Bridge_Type: bridgeT,
Scour_Index: scour,
Flood_Scenario: inundationDepth,
Damage: bridgeDamPrcnt * 100,
Damage: parseInt(bridgDam).toLocaleString(),
Functionality: parseInt(bridgeFunction * 100),
});
return val.replaceAll(",", "<br>").replaceAll(/|{|}/g,"");
},
},
data: bridges_layer,
});
}
/*
* Begin Rendering Utilities Layer
*/
// Utility data exists for Cedar Falls only
if (this.city === "Cedar Falls") {
// Get utilities damage functions data
let utilityDamFun = await floodDM.getUtilityDamageFunction();
await Layers({
args: {
type: "geojson",
name: "Utilities Layer",
// Function to set style
styleFunction: (feature) => {
// Destructure properties according to features based on maptype
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
let utilityDam = 0,
inundationDepth,
utilityType,
utilityDamPrcnt;
let depth100 = properties.depth100;
let depth500 = properties.depth500;
let utilitySystem = properties.UtilFcltyC;
let utilityCost = properties.Cost;
let floodScenario = this.scenario;
if (floodScenario === "x100_year") {
inundationDepth = depth100;
} else if (floodScenario === "x500_year") {
inundationDepth = depth500;
}
for (let i in utilityDamFun[utilitySystem]) {
if (
utilityDamFun[utilitySystem][i]["floodDepth"] ===
inundationDepth
) {
utilityDamPrcnt = utilityDamFun[utilitySystem][i]["damage"];
utilityType = utilityDamFun[utilitySystem][i]["utilitySystem"];
}
}
utilityDam = (utilityDamPrcnt / 100) * utilityCost * 1000;
//Update scenario variable
if (!isNaN(utilityDam))
scenario_results["Utilities"]["Damage"] += utilityDam;
return {
scale: 2,
radius: 2,
weight: 10,
fillColor: "#F2B679",
color: "#F2B679",
fillOpacity: 0.5,
strokeWeight: 0.7,
};
},
// Set popup properties based on feature properties
popUpFunction: (feature) => {
// Destructure properties according to features based on maptype
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
// Calculate utilities values
let utilityDam = 0,
inundationDepth,
utilityType,
utilityDamPrcnt;
let depth100 = properties.depth100;
let depth500 = properties.depth500;
let utilitySystem = properties.UtilFcltyC;
let utilityCost = properties.Cost;
let floodScenario = this.scenario;
if (floodScenario === "x100_year") {
inundationDepth = depth100;
} else if (floodScenario === "x500_year") {
inundationDepth = depth500;
}
for (let i in utilityDamFun[utilitySystem]) {
if (
utilityDamFun[utilitySystem][i]["floodDepth"] ===
inundationDepth
) {
utilityDamPrcnt = utilityDamFun[utilitySystem][i]["damage"];
utilityType = utilityDamFun[utilitySystem][i]["utilitySystem"];
}
}
utilityDam = (utilityDamPrcnt / 100) * utilityCost * 1000;
// Set popup values
return JSON.stringify({
"Utiltiy ID": properties.WasteWater,
"Utility Type": utilityType,
"Flood depth": inundationDepth,
Damage: parseInt(utilityDam),
}).replaceAll(",", "<br>").replaceAll(/|{|}/g,"");
},
},
data: utilities_layer,
});
}
/*
* Create and display a legend on the map
*/
// Create legend div for the map
const legendHTML = `<div id="legend" class="content"
style="display: block; background: rgba(255, 255, 255, 1); padding: 2px 5px; border-style: solid !important;">
<b>Legend</b>
<br>
<span>●</span> Bridge
<br>
<span style="color: orange;">●</span> Utility
<br>
<span style="background-color: blue; color: blue;">●</span> Flood extent
<br>
<b>Building Damage ($)</b>
<br>
<span style="color: green;">●</span> 0 - 100k
<br>
<span style="color: yellow;">●</span> 100k - 1M
<br>
<span style="color: red;">●</span> 1M+
<br>
<b>Vehicle Damage ($)</b>
<br>
<span style="background-color: #BFBFBF; color: #BFBFBF;">●</span> 0 - 100k
<br>
<span style="background-color: #7E7E7E; color: #7E7E7E;">●</span> 100k - 1M
<br>
<span style="background-color: #323232; color: #323232;">●</span> 1M+
</div>`;
// Call the addCustomLegend function for Maps
const legendDiv = document.createElement("div");
legendDiv.innerHTML = legendHTML;
await addCustomLegend({
params: {
position: "bottom left",
},
args: {
div: legendDiv,
},
});
/*
* Create and display a overlay to display scenario summary on the map
*/
// Format results before printing
scenario_results["Vehicles"]["Day"] = floodDM.formatUSDollar(
scenario_results["Vehicles"]["Day"]
);
scenario_results["Vehicles"]["Night"] = floodDM.formatUSDollar(
scenario_results["Vehicles"]["Night"]
);
scenario_results["Buildings"]["Structure"] = floodDM.formatUSDollar(
scenario_results["Buildings"]["Structure"]
);
scenario_results["Buildings"]["Content"] = floodDM.formatUSDollar(
scenario_results["Buildings"]["Content"]
);
scenario_results["Buildings"]["Income"] = floodDM.formatUSDollar(
scenario_results["Buildings"]["Income"]
);
scenario_results["Buildings"]["Wage"] = floodDM.formatUSDollar(
scenario_results["Buildings"]["Wage"]
);
scenario_results["Buildings"]["Relocation Expenses"] =
floodDM.formatUSDollar(
scenario_results["Buildings"]["Relocation Expenses"]
);
scenario_results["Buildings"]["Rental Income"] = floodDM.formatUSDollar(
scenario_results["Buildings"]["Rental Income"]
);
scenario_results["Utilities"]["Damage"] = floodDM.formatUSDollar(
scenario_results["Utilities"]["Damage"]
);
scenario_results["Buildings"]["Loss of Life (Day)"] = Math.floor(
scenario_results["Buildings"]["Loss of Life (Day)"]
);
scenario_results["Buildings"]["Loss of Life (Night)"] = Math.floor(
scenario_results["Buildings"]["Loss of Life (Day)"]
);
// Create HTML table using the scenario summary
let city_damage_table = floodDM.createTableFromObject({
args: { obj: scenario_results },
});
// Create enclosing div for the scenario summary
let city_damage_div = document.createElement("div");
city_damage_div.appendChild(city_damage_table);
city_damage_div.style.position = "absolute";
city_damage_div.style.float = "left";
city_damage_div.style.top = "50px";
city_damage_div.style.left = "50px";
city_damage_div.style.display = "block";
city_damage_div.style.backgroundColor = "white";
city_damage_div.style.fontSize = "x-small";
floodDM.appendCloseButton(city_damage_div);
// Add the div to the HTML page
document.body.appendChild(city_damage_div);
// return scenario summary
return scenario_results;
}
// Helper function to attach close button on div
static appendCloseButton(div) {
const cross = document.createElement("span");
cross.classList.add("close-btn");
cross.onclick = function () {
this.parentNode.remove();
return false;
};
// Set CSS styles
const styles = {
position: "absolute",
top: "10px",
right: "10px",
width: "20px",
height: "20px",
cursor: "pointer",
};
Object.assign(cross.style, styles);
// Create pseudo-elements for the cross
const beforeAfterStyles = `
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 2px;
background-color: #777;
transform: translate(-50%, -50%) rotate(45deg);
`;
const hoverStyles = `
background-color: #000000;
`;
cross.appendChild(document.createElement("style")).textContent = `
.close-btn::before,
.close-btn::after {
${beforeAfterStyles}
}
.close-btn::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.close-btn:hover::before,
.close-btn:hover::after {
${hoverStyles}
}
`;
// Make sure the parent div has a relative position
div.style.position = "relative";
// Append the close button to the div
div.appendChild(cross);
}
// Helper function for initDamageScenario to get property loss data by depth and type
static async getPropertyDamageFunction({ params, args, data } = {}) {
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const propertyLossesUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/business_str_cnt_inputs.js";
const proxyUrl = `${corsProxyUrl}${propertyLossesUrl}`;
try {
const response = await fetch(proxyUrl);
let propertyLosses = await response.text();
eval(propertyLosses);
return propertyLosses;
} catch (error) {
console.error("Error fetching property losses data:", error);
return null;
}
}
// Helper function for initDamageScenario for life loss by depth
static async getLifeLossFunction({ params, args, data } = {}) {
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const lifeLossesUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/life_inputs.js";
const proxyUrl = `${corsProxyUrl}${lifeLossesUrl}`;
try {
const response = await fetch(proxyUrl);
let life = await response.text();
eval(life);
return life;
} catch (error) {
console.error("Error fetching life losses data:", error);
return null;
}
}
// Helper function for initDamageScenario for debris loss by depth and property type
static async getDebrisWeightFunction({ params, args, data } = {}) {
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const debrisUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/debris_weight.js";
const proxyUrl = `${corsProxyUrl}${debrisUrl}`;
try {
const response = await fetch(proxyUrl);
let debris = await response.text();
eval(debris);
return debris;
} catch (error) {
console.error("Error fetching debris weight data:", error);
return null;
}
}
// Helper function for initDamageScenario for bridge loss by depth
static async getBridgeDamageFunction({ params, args, data } = {}) {
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const debrisUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/bridge_function.js";
const proxyUrl = `${corsProxyUrl}${debrisUrl}`;
try {
const response = await fetch(proxyUrl);
let bridgeDamFun = await response.text();
eval(bridgeDamFun);
return bridgeDamFun;
} catch (error) {
console.error("Error fetching debris weight data:", error);
return null;
}
}
// Helper function for initDamageScenario for vehicle loss by depth
static async getVehicleDamageFunction({ params, args, data } = {}) {
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const debrisUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/vehicle_function.js";
const proxyUrl = `${corsProxyUrl}${debrisUrl}`;
try {
const response = await fetch(proxyUrl);
let vehicleDamFun = await response.text();
eval(vehicleDamFun);
return vehicleDamFun;
} catch (error) {
console.error("Error fetching debris weight data:", error);
return null;
}
}
// Helper function for initDamageScenario for utility loss by depth
static async getUtilityDamageFunction({ params, args, data } = {}) {
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const debrisUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/utility_function.js";
const proxyUrl = `${corsProxyUrl}${debrisUrl}`;
try {
const response = await fetch(proxyUrl);
let utilityDamFun = await response.text();
eval(utilityDamFun);
return utilityDamFun;
} catch (error) {
console.error("Error fetching debris weight data:", error);
return null;
}
}
/**
* Initialize flood mitigation scenario
* @method initMitigationScenario
* @memberof floodDM
* @instance
* @async
* @param {String} args.city - City name for which mitigation scenario needs to be built. Allowed cities are Waterloo, Cedar Rapids or Cedar Falls
* @param {number} args.depth - The flood depth for which to retrieve damage data.
* @example
* hydro.analyze.hydro.initMitigationScenario({
* args: { city : "Waterloo", depth: 30.5 }
* });
*/
async initMitigationScenario({ params, args, data } = {}) {
// Set class members of city and depth
let { city, depth } = args;
this.city = city;
this.depth = depth;
// Check params for maptype
if (params === undefined) {
this.maptype = "leaflet";
params = {};
} else if (params.maptype === undefined) this.maptype = "leaflet";
else this.maptype = params.maptype;
if (this.maptype === "google") {
if (params.key === undefined) {
this.maptype = "leaflet";
}
}
// Get building features for mitigation features
let city_features = await retrieve({
params: {
source: "mitigation_dt",
datatype: "community_flood_inundation",
},
args: { sourceType: this.city },
});
// Calculate centroid using the building features
let centroid = await floodDM.featureCollectionCentroid(city_features);
// Filter the city features retrived to get features applicable at the particular flood depth
city_features = await floodDM.getFloodInundation({
args: { depth: this.depth },
data: city_features,
});
// Render the map based on parameters
if (!this.mapRendered) {
await renderMap({
params: { maptype: this.maptype, lat: centroid[1], lon: centroid[0] },
args: { key: params.key },
});
this.mapRendered = true;
}
// If any building features exist
if (city_features.features.length > 0) {
// Define style function for the features
let styleFunction = (feature) => {
let color = "gray",
depth_style,
radius;
// Define current scenario
let current_scenario = "depth" + String(Math.floor(this.depth * 10));
// Get properties specific map type
if (this.maptype === "google") {
depth_style = feature.getProperty(current_scenario);
radius = 1;
} else if (this.maptype === "leaflet") {
depth_style = feature.properties[current_scenario];
radius = 4;
}
// Set marker color based on depth of the building
if (5 < depth_style && depth_style <= 10) color = "purple";
else if (2 < depth_style && depth_style <= 5) color = "red";
else if (0 < depth_style && depth_style <= 2) color = "yellow";
// Return style based on feature properties
return {
scale: 5,
radius: radius,
fillColor: color,
color: "black",
weight: 1,
opacity: 1,
fillOpacity: 0.8,
strokeWeight: 0.7,
};
};
// Define popup function for the features
let popUpFunction = (feature) => {
let current_scenario = "depth" + String(Math.floor(this.depth * 10));
// Destructure the properties based on maptype
let properties;
if (this.maptype === "google") {
properties = {};
feature.forEachProperty(function (value, property) {
properties[property] = value;
});
} else if (this.maptype === "leaflet") {
properties = feature.properties;
}
return `<h4><b>Damage and Mitigation</b><br></h4>
<b>Building Information</b><br>
Building ID: ${String(properties.gid)}<br>
Occupancy Type: ${String(properties.occupancy2)}<br>
Flood Depth: ${properties[current_scenario]} ft<br>`;
};
// Define click function for the features
let onClickFunction = async (event) => {
if (this.maptype === "google") {
this.selectedfeature = event.feature;
} else if (this.maptype === "leaflet") {
this.selectedfeature = event.target.feature;
}
console.log(this.selectedfeature);
};
// Render Layer for building features
await Layers({
args: {
type: "geojson",
name: "mygeoJSON",
styleFunction,
popUpFunction,
onClickFunction,
},
data: city_features,
});
}
// Get city and depth specific property damage values
let city_damage = await floodDM.getCityFloodDamage({
args: { depth: this.depth, city: this.city },
});
// Hard code emissions data for various cities as these values were hardcoded in original MiDAS system
if (city === "Cedar Rapids") {
city_damage["low_emission"] = "$ 295.3 M";
city_damage["high_emission"] = "$ 635.8 M";
} else if (city === "Cedar Falls") {
city_damage["low_emission"] = "$ 175.5 M";
city_damage["high_emission"] = "$ 332.9 M";
} else if (city === "Waterloo") {
city_damage["low_emission"] = "$ 269.2 M";
city_damage["high_emission"] = "$ 785.6 M";
}
// Create an HTML table for overlay of mitigation summary
let city_damage_table = floodDM.createTableFromObject({
args: { obj: city_damage, tableHeaderText: this.city },
});
// Create a wrapper div for table
let city_damage_div = document.createElement("div");
city_damage_div.appendChild(city_damage_table);
city_damage_div.style.position = "absolute";
city_damage_div.style.float = "left";
city_damage_div.style.top = "50px";
city_damage_div.style.left = "50px";
city_damage_div.style.display = "block";
city_damage_div.style.backgroundColor = "white";
city_damage_div.style.fontSize = "x-small";
floodDM.appendCloseButton(city_damage_div);
document.body.appendChild(city_damage_div);
// Create a HTML div
const legendHTML = `
<div id="legend" class="content"
style="display: block; background: rgba(255, 255, 255, 1); padding: 2px 5px; border-style: solid !important;">
">
<b>LEGEND</b>
<br>
<b>Depth</b>
<br>
<span style="color: purple;">■</span> > 5 - 10 ft</span>
<br>
<span style="color: red;">■</span> 2 - 5 ft</span>
<br>
<span style="color: yellow;">■</span> < 2 ft</span>
</div>
`;
const legendDiv = document.createElement("div");
legendDiv.innerHTML = legendHTML;
// Call the addCustomLegend function for Maps
await addCustomLegend({
params: {
position: "bottom right",
},
args: {
div: legendDiv,
},
});
// Return summary of city damage
return city_damage;
}
// Helper function to calculate centroid for Point feature collections.
// Pass an array of features
static featureCollectionCentroid(featureCollection) {
let numFeatures = featureCollection.features.length;
let coordSum = featureCollection.features.reduce(
(runningSum, currentFeature) => {
return [
currentFeature.geometry.coordinates[0] + runningSum[0],
currentFeature.geometry.coordinates[1] + runningSum[1],
];
},
[0, 0]
);
return [coordSum[0] / numFeatures, coordSum[1] / numFeatures];
}
/**
* Run the damage and mitigation scenario of the selected property based on flood depth and mitigation parameters
*
* @method runMitigationScenario
* @memberof floodDM
* @instance
* @async
* @param {String} args.city - City name for which mitigation scenario needs to be built. Allowed cities are Waterloo, Cedar Rapids or Cedar Falls
* @param {number} args.depth - The flood depth for which to retrieve damage data.
* @example
* hydro.analyze.hydro.initMitigationScenario({
* args: { city : "Waterloo", depth: 30.5 }
* });
*/
async runMitigationScenario({ params, args, data } = {}) {
let { mitigationMeasure, mitigationDepth, foundationType } = args;
let props;
//Check if a building feature has been selected
if (this.selectedfeature === null) {
console.log(
"Click on a property marker and then run mitigation scenario"
);
return;
}
// Destructure feature prooperties based on map
// The properties are retrieved from the property selected by click on the map
if (this.maptype === "google") {
props = {};
this.selectedfeature.forEachProperty(function (value, property) {
props[property] = value;
});
} else if (this.maptype === "leaflet") {
props = this.selectedfeature.properties;
}
// Set params based on properties of the selected feature
params = {
occupancy: props.occupancy2,
structuralValue: props.val_struct,
contentValue: props.val_cont,
buildingArea: props.sqfoot,
};
// Set args based on properties of the selected feature
args = {
floodDepth: props["depth" + (this.depth * 10).toString()],
mitigationMeasure: mitigationMeasure,
mitigationDepth: mitigationDepth,
foundationType: foundationType,
};
// Build flood damage and mitigation scenario based on provided parameters
let mitigationResult = await floodDM.buildPropertyDMScenario({
params,
args,
data,
});
// Create HTML table using the damage and mitigation metrics of the buildings
let city_damage_table = floodDM.createTableFromObject({
args: {
obj: mitigationResult,
tableHeaderText: "Damage and Mitigation Summary",
},
});
// Create wrapper for HTML table
let city_damage_div = document.createElement("div");
city_damage_div.appendChild(city_damage_table);
city_damage_div.style.position = "absolute";
city_damage_div.style.float = "left";
city_damage_div.style.top = "200px";
city_damage_div.style.left = "50px";
city_damage_div.style.display = "block";
city_damage_div.style.backgroundColor = "white";
city_damage_div.style.fontSize = "x-small";
floodDM.appendCloseButton(city_damage_div)
// Add div to page
document.body.appendChild(city_damage_div);
// Return summary
return city_damage_table;
}
/**
* Retrieves flood damage data for a given flood depth.
*
* @method getCityFloodDamage
* @memberof floodDM
* @async
* @param {String} args.city - An array of objects containing flood damage data for each flood level.
* @param {number} args.depth - The flood depth for which to retrieve damage data.
* @returns {Object} An object containing the number of buildings affected, the structural loss, and the content loss for the closest matching flood level.
* @example
* hydro.analyze.hydro.getCityFloodDamage({
* args: { depth: 30.5, city: "Cedar Rapids" }
* });
*/
static async getCityFloodDamage({ params, args, data } = {}) {
// Validate input data
if (!args.city || !args.depth) {
return "City or depth not found";
}
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
let cityname;
if (args.city === "Cedar Falls") {
cityname = "cedar_falls";
} else if (args.city === "Cedar Rapids") {
cityname = "cedar_rapids";
} else if (args.city === "Waterloo") {
cityname = "waterloo";
}
const debrisUrl = `https://hydroinformatics.uiowa.edu/lab/midas/communities/${cityname}.json`;
const proxyUrl = `${corsProxyUrl}${debrisUrl}`;
try {
const response = await fetch(proxyUrl);
let cityDamage = await response.json();
let damage = cityDamage["city"].filter(
(item) => item["flood_level"] === args.depth.toString()
)[0];
return damage;
} catch (error) {
console.error("Error fetching debris weight data:", error);
return null;
}
}
/**
*
* Helper function to create an HTML table element from a JavaScript object.
* @method createTableFromObject
* @memberof floodDM
* @private
* @param {Object} args - An object containing the arguments for the function.
* @param {Object} args.obj - The JavaScript object to convert to an HTML table.
* @param {number} args.depth=0 - The depth level of the current object (used for indentation).
* @param {boolean} args.showHeader=true - Whether to display the object keys as table headers (bold text).
* @param {string|null} args.tableHeaderText=null - The text to display in the table header row (if provided).
* @returns {HTMLTableElement} The HTML table element created from the object.
*/
static createTableFromObject({ params, args, data } = {}) {
if (args.obj === undefined) args.obj = "";
let { obj, depth = 0, showHeader = true, tableHeaderText = null } = args;
// Create the table element
const table = document.createElement("table");
table.style.borderCollapse = "collapse"; // Collapse borders for better lines
table.style.border = "1px solid #fff"; // Add border to the table
// Add table header row if tableHeaderText is provided
if (tableHeaderText !== null && depth === 0) {
const headerRow = table.insertRow();
const headerCell = headerRow.insertCell();
headerCell.textContent = tableHeaderText;
headerCell.style.fontWeight = "bold";
headerCell.style.fontFamily = "Arial";
headerCell.colSpan = 2;
headerCell.style.borderRight = "1px solid #000";
}
// Loop through the object keys
for (const key in obj) {
// Create a new row
const row = table.insertRow();
// Insert a cell for the key
const keyCell = row.insertCell();
keyCell.textContent = key;
keyCell.style.padding = "0 10px 0 10px"; // Indent based on depth
keyCell.style.borderRight = "1px solid #fff"; // Add right border to key cell
keyCell.style.backgroundColor = "#337bcb";
keyCell.style.color = "white";
keyCell.style.fontFamily = "Arial";
// If showHeader is true, make the key bold
if (showHeader) {
keyCell.style.fontWeight = "bold";
}
// Insert a cell for the value
const valueCell = row.insertCell();
// If the value is an object, create a nested table
if (typeof obj[key] === "object" && obj[key] !== null) {
valueCell.style.borderBottom = "1px solid #fff"; // Add bottom border to value cell
valueCell.style.backgroundColor = "#fff";
valueCell.appendChild(
floodDM.createTableFromObject({
args: { obj: obj[key], depth: depth + 1, showHeader: true },
})
);
} else {
valueCell.textContent = obj[key];
}
}
return table;
}
/**
* Returns an array of GeoJSON point features of Buildings from midas data source and community_flood_inundation datatype
* based on flood depth
*
* @method getFloodInundation
* @memberof floodDM
* @param {Object[]} data - An array of objects containing flood damage data for each flood level.
* @param {number} args.depth - The flood depth for which to retrieve inundation data.
* @returns {Object} An object containing the number of buildings affected for the closest matching flood level.
* @example
* hydro.analyze.hydro.getFloodInundation({ args:{depth: 20 }, data: {
* "type": "FeatureCollection",
* "name": "waterloo",
* "crs": {
* "type": "name",
* "properties": {
* "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
* }
* },
* "features": [
* {
* "type": "Feature",
* "properties": {
* ...
* },
* "geometry": {
* "type": "Point",
* "coordinates": [
* -92.39561119522634,
* 42.47028021204937
* ]
* }
* },...
*/
static getFloodInundation({ params, args, data } = {}) {
// Validate input data
if (!data.features || !Array.isArray(data.features)) {
return { error: "Invalid data format" };
}
if (!args || typeof args.depth !== "number") {
return { error: "Invalid args format" };
}
const floodDepth = args.depth;
let dataClone = structuredClone(data);
dataClone.features = dataClone.features.filter((item) => {
return (
item.properties["depth" + String(Math.floor(floodDepth * 10))] >= 1
);
});
return dataClone;
}
/**
* Calculates the property details and optionally retrieves flood damage and mitigation options.
*
* @method buildPropertyDMScenario
* @memberof floodDM
* @async
* @param {Object} params - An object containing additional parameters (currently unused).
* @param {Object} args - An object containing the property details and optional flood depth and mitigation depth.
* @param {string} params.occupancy - The occupancy type (e.g., 'RES1', 'COM1', 'IND1', 'GOV1', 'AGR1').
* @param {number} params.structuralValue - The structural value of the property.
* @param {number} params.contentValue - The content value of the property.
* @param {number} params.buildingArea - The building area of the property.
* @param {number} [args.floodDepth] - The flood depth for which to calculate the damage (optional).
* @param {number} [args.mitigationDepth] - The desired mitigation measure (optional).
* @param {number} [args.mitigationDepth] - The desired mitigation depth (optional).
* @param {Object} data - Additional data (currently unused).
* @returns {Promise<Object>} A Promise that resolves with an object containing the property details, and optionally the flood depth, calculated losses, and mitigation options.
* @example
* const propertyDetails = await floodDM.buildPropertyDMScenario({}, { occupancy: 'RES1', structuralValue: 200000, contentValue: 100000, buildingArea: 2000 }, {});
* console.log(propertyDetails);
*
* const propertyDamage = await floodDM.buildPropertyDMScenario({}, { occupancy: 'RES1', structuralValue: 200000, contentValue: 100000, buildingArea: 2000, floodDepth: 3 }, {});
* console.log(propertyDamage);
*
* const propertyDamageAndMitigation = await floodDM.buildPropertyDMScenario({}, { occupancy: 'RES1', structuralValue: 200000, contentValue: 100000, buildingArea: 2000, floodDepth: 3, mitigationDepth: 2 }, {});
* console.log(propertyDamageAndMitigation);
*/
static async buildPropertyDMScenario({ params, args, data } = {}) {
// Destructure the required parameters and arguments
const { occupancy, structuralValue, contentValue, buildingArea } = params;
const { floodDepth, mitigationMeasure, mitigationDepth, foundationType } =
args;
// Create an object to store the result
const result = {
occupancy,
structuralValue,
contentValue,
buildingArea,
};
// Check if floodDepth is a number
if (typeof floodDepth === "number") {
// Fetch the damage data based on occupancy and flood depth
const damageData = await this.fetchCurvesData(occupancy, floodDepth);
// Calculate structural and content losses based on the damage data
const losses = this.calculateLosses(
structuralValue,
contentValue,
damageData
);
// Add flood depth, structural loss, and content loss to the result object
result.floodDepth = floodDepth;
result.structuralLoss = losses.structuralLoss;
result.contentLoss = losses.contentLoss;
let finalSqft = result.buildingArea;
// Check if mitigation measure and depth are provided
if (mitigationDepth && mitigationMeasure) {
// Get mitigation options based on foundation type, mitigation measure, mitigation depth, and building area
const mitigationOptions = await this.getMitigationOptions(
foundationType,
mitigationMeasure,
mitigationDepth,
finalSqft,
result
);
// Add mitigation options to the result object
result.mitigationOptions = mitigationOptions;
}
// Formatting loss values as US Dollar values
result.structuralLoss = floodDM.formatUSDollar(result.structuralLoss);
result.contentLoss = floodDM.formatUSDollar(result.contentLoss);
result.structuralValue = floodDM.formatUSDollar(result.structuralValue);
result.contentValue = floodDM.formatUSDollar(result.contentValue);
}
// Return the result object
return result;
}
// Helper function for buildPropertyDMScenario to retrieve curves.json
// which contains property and depth specific damage per unit area data
static async fetchCurvesData(occupancy, floodDepth) {
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const curvesUrl =
"https://hydroinformatics.uiowa.edu/lab/midas/jsons/curves.json";
const proxyUrl = `${corsProxyUrl}${curvesUrl}`;
let response = await fetch(proxyUrl);
response = await response.json();
const occupancyData = response.curves.filter(
(curve) =>
curve.occupancy === occupancy && curve.depth === floodDepth.toString()
);
const closestDepth = occupancyData.reduce((prev, curr) =>
Math.abs(curr.depth - floodDepth) < Math.abs(prev.depth - floodDepth)
? curr
: prev
);
return closestDepth;
}
// Helper function for buildPropertyDMScenario to
// calculate the structural and content loss given curves data and property value
static calculateLosses(structuralValue, contentValue, damageData) {
const structuralLoss = (structuralValue * damageData.struct_dam_per) / 100;
const contentLoss = (contentValue * damageData.cont_dam_per) / 100;
return {
structuralLoss: Math.round(structuralLoss / 1000) * 1000,
contentLoss: Math.round(contentLoss / 1000) * 1000,
};
}
// Helper function for buildPropertyDMScenario to
// build mitigation scenario for selected property
static async getMitigationOptions(
foundationType,
mitigationMeasure,
mitigationDepth,
finalSqft,
result
) {
// Fetch mitigation data
const mitigationData = await this.fetchMitigationData();
let final_mitigation_cost, final_mitigation_benefit;
// Check if mitigation data is available
if (!mitigationData) {
return { error: "Failed to fetch mitigation data" };
}
const mitigationOptions = mitigationData.mitigation_options;
// Get structural loss, content loss, and final depth from the result object
let structuralLoss = result.structuralLoss;
let contentLoss = result.contentLoss;
let finalDepth = result.floodDepth;
// Iterate over each mitigation option
for (let option of mitigationOptions) {
// Check if the mitigation measure matches the option and the foundation type matches
if (
mitigationMeasure == option.measure &&
option.foundation_type === foundationType
) {
// Calculate mitigation cost and benefit for this option
final_mitigation_cost = finalSqft * option.cost;
final_mitigation_cost = Math.round(final_mitigation_cost / 1000) * 1000;
final_mitigation_benefit =
structuralLoss + contentLoss - final_mitigation_cost;
final_mitigation_benefit =
Math.round(final_mitigation_benefit / 1000) * 1000;
}
// Check if the mitigation measure is "Wet Floodproofing" and the mitigation depth matches the option design
else if (
mitigationMeasure == "Wet Floodproofing" &&
mitigationDepth == option.design
) {
// Calculate mitigation cost and benefit for this option
var perimeter = Math.sqrt(finalSqft);
final_mitigation_cost = 4 * perimeter * option.cost;
final_mitigation_cost = Math.round(final_mitigation_cost / 1000) * 1000;
if (mitigationDepth < finalDepth) {
final_mitigation_benefit =
-final_mitigation_cost - structuralLoss - contentLoss;
final_mitigation_benefit =
Math.round(final_mitigation_benefit / 1000) * 1000;
} else if (mitigationDepth >= finalDepth) {
final_mitigation_benefit =
structuralLoss + contentLoss - final_mitigation_cost - contentLoss;
final_mitigation_benefit =
Math.round(final_mitigation_benefit / 1000) * 1000;
}
} else {
// Check if the mitigation measure and mitigation depth match the option
if (
mitigationMeasure == option.measure &&
mitigationDepth == option.design
) {
// Calculate mitigation cost and benefit based on the application type
if (option.app_type == "linear") {
var perimeter = Math.sqrt(finalSqft);
final_mitigation_cost = 4 * perimeter * option.cost;
final_mitigation_cost =
Math.round(final_mitigation_cost / 1000) * 1000;
if (mitigationDepth < finalDepth) {
final_mitigation_benefit =
-final_mitigation_cost - structuralLoss - contentLoss;
final_mitigation_benefit =
Math.round(final_mitigation_benefit / 1000) * 1000;
} else if (mitigationDepth >= finalDepth) {
final_mitigation_benefit =
structuralLoss + contentLoss - final_mitigation_cost;
final_mitigation_benefit =
Math.round(final_mitigation_benefit / 1000) * 1000;
}
} else if (option.app_type == "area") {
final_mitigation_cost = finalSqft * option.cost;
final_mitigation_cost =
Math.round(final_mitigation_cost / 1000) * 1000;
if (mitigationDepth < finalDepth) {
final_mitigation_benefit =
-final_mitigation_cost - structuralLoss - contentLoss;
final_mitigation_benefit =
Math.round(final_mitigation_benefit / 1000) * 1000;
} else if (mitigationDepth >= finalDepth) {
final_mitigation_benefit =
structuralLoss + contentLoss - final_mitigation_cost;
final_mitigation_benefit =
Math.round(final_mitigation_benefit / 1000) * 1000;
}
}
}
}
}
// Create a return object with mitigation measure, foundation type, cost, and benefit
let returnVal = {
measure: mitigationMeasure,
foundationType: foundationType,
cost: floodDM.formatUSDollar(final_mitigation_cost),
benefit: floodDM.formatUSDollar(final_mitigation_benefit),
};
return returnVal;
}
// Helper function to convert number values to US $ formatted strings
static formatUSDollar(value) {
// Check if the input is a valid number
if (isNaN(value) || typeof value !== "number") {
return "Invalid input";
}
// Format the number with two decimal places and commas for thousands separator
const formattedValue = value.toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
return formattedValue;
}
// Helper function for getPropertyDmMt
static async fetchMitigationData() {
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const mitigationUrl =
"https://hydroinformatics.uiowa.edu/lab/midas/jsons/mitigations.json";
const proxyUrl = `${corsProxyUrl}${mitigationUrl}`;
try {
const response = await fetch(proxyUrl);
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching mitigation data:", error);
return null;
}
}
/**
* Retrieves bridge damage data based on the provided arguments.
*
* @method getBridgeDamage
* @memberof floodDM
* @async
* @param {Object} params - An object containing additional parameters (currently unused).
* @param {Object} args - An object containing the required arguments.
* @param {string} args.bridge_type - The type of the bridge, either "Single Span" or "Continuous Span".
* @param {string} args.scour_index - The scour index, which can be "Unknown", "1", "2", or "3".
* @param {string} args.flood_scenario - The flood scenario, from "25 yr" to "500 yr" in 25-year intervals.
* @param {number} args.replacement_value - The replacement value of the bridge.
* @param {Object} data - Additional data (currently unused).
* @returns {Promise<Object|null>} A Promise that resolves with an object containing the bridge damage data, or null if no matching data is found or an error occurs.
*/
static async getBridgeDamage({ params, args, data } = {}) {
// Destructure the required arguments from the args object
const { bridge_type, scour_index, flood_scenario, replacement_value } =
args;
// Define the URL for fetching bridge damage data and construct the proxy URL
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const bridgeDamageUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/bridge_function.js";
const proxyUrl = `${corsProxyUrl}${bridgeDamageUrl}`;
try {
// Fetch the data from the proxy URL
const response = await fetch(proxyUrl);
const data = await response.text();
// Extract the bridgeDamFun object from the fetched data
let bridgeDamFun;
eval(data);
// Find the matching entry based on bridge_type, scour_index, and flood_scenario
const matchingEntry = bridgeDamFun[bridge_type].find(
(entry) =>
entry["ScourPotential"] === scour_index &&
entry["Flood Scenario"] === flood_scenario
);
if (matchingEntry) {
// If a matching entry is found, calculate and return the bridge damage data
const damagePercent = matchingEntry["damage prcnt"];
const damageCost = replacement_value * damagePercent;
const functionalCost = replacement_value * (1 - functionalPercent);
return {
damagePercent,
damageCost: Math.round(damageCost),
damage: damage,
};
} else {
// If no matching entry is found, log a warning and return null
console.warn(
`No matching entry found for bridge_type: ${bridge_type}, scour_index: ${scour_index}, and flood_scenario: ${flood_scenario}`
);
return null;
}
} catch (error) {
// If an error occurs during the data fetch, log the error and return null
console.error("Error fetching bridge damage data:", error);
return null;
}
}
/**
* Retrieves utility damage data based on the provided arguments.
*
* @method getUtilityDamage
* @memberof floodDM
* @async
* @param {Object} params - An object containing additional parameters (currently unused).
* @param {Object} args - An object containing the required arguments.
* @param {string} args.utility - The utility system (e.g., "PDFLT", "PPPL", "WWTL", etc.).
* @param {number} args.depth - The flood depth in feet.
* @param {number} args.utilityValue - The replacement value of the utility.
* @param {Object} data - Additional data (currently unused).
* @returns {Promise<Object|null>} A Promise that resolves with an object containing the utility damage data, or null if no matching data is found or an error occurs.
*/
static async getUtilityDamage({ params, args, data } = {}) {
// Destructure the required arguments from the args object
const { utility, depth, utilityValue } = args;
// Define the URL for fetching utility damage data and construct the proxy URL
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const utilityDamageUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/utility_function.js";
const proxyUrl = `${corsProxyUrl}${utilityDamageUrl}`;
try {
// Fetch the data from the proxy URL
const response = await fetch(proxyUrl);
const data = await response.text();
// Extract the utilityDamFun object from the fetched data
let utilityDamFun;
eval(data);
// Find the matching entry based on utility and depth
const matchingEntry = utilityDamFun[utility].find(
(entry) => entry.floodDepth === depth
);
if (matchingEntry) {
// If a matching entry is found, calculate and return the utility damage data
const damagePercent = matchingEntry.damage;
const damageCost = utilityValue * (damagePercent / 100);
return {
damagePercent,
damageCost: Math.round(damageCost),
};
} else {
// If no matching entry is found, log a warning and return null
console.warn(
`No matching entry found for utility: ${utility} and depth: ${depth}`
);
return null;
}
} catch (error) {
// If an error occurs during the data fetch, log the error and return null
console.error("Error fetching utility damage data:", error);
return null;
}
}
/**
* Retrieves property loss data, including structure and content damage, business interruption losses,
* and debris amount based on the provided arguments.
*
* @method getPropertyLoss
* @memberof floodDM
* @async
* @param {Object} params - An object containing additional parameters (currently unused).
* @param {Object} args - An object containing the required arguments.
* @param {string} args.occupancy - The occupancy type (e.g., 'RES1-1SNB', 'COM1', 'IND1', etc.).
* @param {number} args.depth - The flood depth in feet.
* @param {string} args.foundationType - The foundation type ('footing' or 'slab on grade').
* @param {number} args.structureValue - The structure value of the property.
* @param {number} args.contentValue - The content value of the property.
* @param {number} args.area - The area of the property in square feet.
* @param {Object} data - Additional data (currently unused).
* @returns {Promise<Object>} A Promise that resolves with an object containing the property loss, business interruption, and debris amount data.
*/
static async getPropertyLoss({ params, args, data } = {}) {
const {
occupancy,
depth,
foundationType,
structureValue,
contentValue,
area,
} = args;
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
let propertyLosses, debris;
const propertyLossesUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/business_str_cnt_inputs.js";
const debrisUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/debris_weight.js";
const [propertyLossesData, debrisData] = await Promise.all([
fetch(`${corsProxyUrl}${propertyLossesUrl}`).then((response) =>
response.text()
),
fetch(`${corsProxyUrl}${debrisUrl}`).then((response) => response.text()),
]);
eval(propertyLossesData);
eval(debrisData);
const propertyLossEntry = propertyLosses[occupancy].find(
(entry) => entry.floodDepth === depth
);
const debrisEntry = debris[occupancy].find(
(entry) =>
entry.floodDepth === depth && entry.foundationType === foundationType
);
if (propertyLossEntry && debrisEntry) {
const structureDamPercent = propertyLossEntry.structure;
const contentDamPercent = propertyLossEntry.content;
const structureLoss = (structureValue * structureDamPercent) / 100;
const contentLoss = (contentValue * contentDamPercent) / 100;
const incomePerDay = propertyLossEntry.incomePerSqftPerDay * area;
const wagePerDay = propertyLossEntry.wagePerSqftPerDay * area;
const rentalIncome =
((1 - propertyLossEntry.percentOwnerOccupied / 100) *
area *
propertyLossEntry.rentalCostPerSqftPerDay *
propertyLossEntry.maxTime *
30) /
1000;
const relocation =
(area *
((1 - propertyLossEntry.percentOwnerOccupied / 100) *
propertyLossEntry.disruptionCostPerSqft +
(propertyLossEntry.percentOwnerOccupied / 100) *
(propertyLossEntry.disruptionCostPerSqft +
propertyLossEntry.rentalCostPerSqftPerDay *
propertyLossEntry.maxTime *
30))) /
1000;
const finishesDebris = (area * debrisEntry.finishes) / 1000;
const structureDebris = (area * debrisEntry.structure) / 1000;
const foundationDebris = (area * debrisEntry.foundation) / 1000;
const totalDebris = finishesDebris + structureDebris + foundationDebris;
return {
propertyLoss: {
structureLoss: Math.round(structureLoss),
contentLoss: Math.round(contentLoss),
structureDamPercent,
contentDamPercent,
},
businessInterruption: {
incomeLoss:
Math.round(
(1 - propertyLossEntry.recapturFactor) * incomePerDay * 30
) * propertyLossEntry.maxTime,
wageLoss:
Math.round(
(1 - propertyLossEntry.recapturFactor) * wagePerDay * 30
) * propertyLossEntry.maxTime,
rentalIncome: Math.round(rentalIncome),
relocation: Math.round(relocation),
},
debrisAmount: {
finishesDebris: Math.round(finishesDebris),
structureDebris: Math.round(structureDebris),
foundationDebris: Math.round(foundationDebris),
totalDebris: Math.round(totalDebris),
},
};
} else {
console.warn(
`No matching entry found for occupancy: ${occupancy}, depth: ${depth}, and foundationType: ${foundationType}`
);
return null;
}
}
/**
* Retrieves loss of life data based on the provided arguments.
*
* @method getLifeLoss
* @memberof floodDM
* @param {Object} params - An object containing additional parameters (currently unused).
* @param {Object} args - An object containing the required arguments.
* @param {string} args.occupancy - The occupancy type (e.g., 'RES1-1SNB', 'COM1', 'IND1', etc.).
* @param {number} args.depth - The flood depth in feet.
* @param {number} args.peopleOver65 - The number of people living in this dwelling over 65.
* @param {number} args.peopleUnder65 - he number of people living in this dwelling under 65.
* @param {Object} data - Additional data (currently unused).
* @returns {Promise<Object|null>} A Promise that resolves with an object containing the utility damage data, or null if no matching data is found or an error occurs.
*/
static async getLifeLoss({ params, args, data } = {}) {
// Destructure the required arguments from the args object
const { occupancy, depth, peopleOver65, peopleUnder65 } = args;
// Define the URL for fetching utility damage data and construct the proxy URL
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const lifeLossUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/life_inputs.js";
const proxyUrl = `${corsProxyUrl}${lifeLossUrl}`;
try {
// Fetch the data from the proxy URL
const response = await fetch(proxyUrl);
const data = await response.text();
// Extract the utilityDamFun object from the fetched data
let life;
eval(data);
// Find the matching entries for over and under 65 based on occupancy and depth
const matchingEntryOver65 = life[occupancy].find(
(entry) => entry.floodDepth === depth && entry.age === "over 65"
);
const matchingEntryUnder65 = life[occupancy].find(
(entry) => entry.floodDepth === depth && entry.age === "under 65"
);
if (matchingEntryOver65 || matchingEntryUnder65) {
return {
lifeLossOver65: parseInt(matchingEntryOver65.zone * peopleOver65),
lifeLossUnder65: parseInt(matchingEntryUnder65.zone * peopleUnder65),
};
}
} catch (error) {
// If an error occurs during the data fetch, log the error and return null
console.error("Error fetching utility damage data:", error);
return null;
}
}
/**
* Fetches vehicle damage information based on flood depth and vehicle type.
*
* @method getVehicleDamage
* @memberof floodDM
* @async
* @param {object} options - Options object containing parameters, arguments, and data.
* @param {object} options.params - Parameters object (not used in this method).
* @param {object} options.args - Arguments object containing floodDepth, vehicleType, vehicleCount, and vehicleValue.
* @param {object} options.data - Additional data (not used in this method).
* @returns {Promise<object|null>} A Promise resolving to an object containing damage and damage percentage, or null if an error occurs.
*/
static async getVehicleDamage({ params, args, data } = {}) {
const { floodDepth, vehicleType, vehicleCount, vehicleValue } = args;
// Construct the URL for fetching vehicle damage data using the proxy
const corsProxyUrl = proxies["local-proxy"]["endpoint"];
const vehicleDamageUrl =
"https://hydroinformatics.uiowa.edu/lab/fidas/data/functions/vehicle_function.js";
const proxyUrl = `${corsProxyUrl}${vehicleDamageUrl}`;
try {
// Fetch the vehicle damage data using the proxy URL
const response = await fetch(proxyUrl);
const data = await response.text();
// Parse the vehicle damage function data
let vehicleDamFun;
eval(data);
// Find the matching entry based on flood depth and vehicle type
const matchingEntry = vehicleDamFun[vehicleType].find(
(entry) => entry.floodDepth === floodDepth
);
if (matchingEntry) {
// If a matching entry is found, calculate damage and damage percentage
const damagePercent = matchingEntry.damage;
const damage = vehicleValue * (damagePercent / 100) * vehicleCount;
return {
damagePercent,
damage: Math.round(damage),
};
} else {
// If no matching entry is found, log a warning and return null
console.warn(
`No matching entry found for vehicleType: ${vehicleType} and floodDepth: ${floodDepth}`
);
return null;
}
} catch (error) {
// If an error occurs during the data fetch, log the error and return null
console.error("Error fetching vehicle damage data:", error);
return null;
}
}
}