import playSound from "../../../sound/playSound.js";
import { map, mapboxgl } from "../../initMap.js";
import { internalBound } from "../../internal/internalBound.js";
let tsunamiFlashInterval = null;
let tsunamiFlashTimeout = null;
let currentTsunamiBounds = null;
export function getTsunamiBounds() {
return currentTsunamiBounds;
}
/**
* Function to clear all tsunami-related layers and intervals.
*
* @returns {void}
* @private
*/
function clearTsunamiLayers() {
if (tsunamiFlashInterval) {
clearInterval(tsunamiFlashInterval);
tsunamiFlashInterval = null;
}
if (tsunamiFlashTimeout) {
clearTimeout(tsunamiFlashTimeout);
tsunamiFlashTimeout = null;
}
if (map.getLayer("tsunamiAreas")) {
map.removeLayer("tsunamiAreas");
}
if (map.getSource("tsunamiAreas")) {
map.removeSource("tsunamiAreas");
}
currentTsunamiBounds = null;
}
/**
* Updates the sidebar with tsunami area information.
*
* @param {*} areas Tsunami areas to be displayed in the sidebar.
* @param {*} geojsonFeatures GeoJSON features containing area information.
* @returns {void}
* @private
*/
function updateTsunamiSidebar(areas, geojsonFeatures) {
const gradeMap = {
MajorWarning: {
containerId: "tsunami-major-warning-list",
color: "#FF00FF",
},
Warning: {
containerId: "tsunami-warning-list",
color: "#FF0000",
},
Watch: {
containerId: "tsunami-watch-list",
color: "#FFFF00",
},
};
Object.values(gradeMap).forEach(({ containerId }) => {
const container = document.getElementById(containerId);
if (container) container.innerHTML = "";
});
const geoMap = new Map();
geojsonFeatures.forEach((feature) => {
geoMap.set(feature.properties.name, feature);
});
const grouped = { MajorWarning: [], Warning: [], Watch: [] };
areas.forEach((area) => {
if (grouped[area.grade]) grouped[area.grade].push(area);
});
Object.entries(grouped).forEach(([grade, areaList]) => {
const { containerId, color } = gradeMap[grade];
const container = document.getElementById(containerId);
if (!container) return;
if (areaList.length === 0) {
const p = document.createElement("p");
p.className = "text-xs text-slate-400";
p.textContent = "No area issued.";
container.appendChild(p);
return;
}
areaList.forEach((area) => {
const feature = geoMap.get(area.name);
const nameEn = feature?.properties?.nameEn || area.name;
const condition = area.firstHeight?.condition || "Unknown";
const arrivalTime = area.firstHeight?.arrivalTime;
const maxHeight =
area.maxHeight?.value != null
? `${parseFloat(area.maxHeight.value).toFixed(1)}m`
: "N/A";
const row = document.createElement("div");
row.className = `border-l-2 py-1.5 pl-3`;
row.style.borderLeftColor = color;
row.innerHTML = `
<div class="flex items-start justify-between">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-white">${nameEn}</p>
<p class="text-xs text-neutral-300">${
arrivalTime
? `First wave is expected to arrive at ${arrivalTime} JST`
: condition === "第1波の到達を確認"
? "First wave confirmed"
: condition === "津波到達中と推測"
? "Wave is expected to be reached"
: condition === "ただちに津波来襲と予測"
? "Immediate tsunami expected"
: condition
}</p>
</div>
<div class="ml-2 text-right">
<p class="text-sm font-medium text-neutral-100">${maxHeight}</p>
</div>
</div>
`;
container.appendChild(row);
});
});
}
/**
* Clears all tsunami-related assets and events, including layers and intervals.
*
* @returns {void}
*/
export function clearAllTsAssets() {
clearTsunamiLayers();
disarmTsComponent();
return;
}
/**
* A part of the main rendering logic for Tsunami (TS) on special logic event recieved by the /jma endpoint of the API.
*
* Renders the Tsunami data on the map and updates the sidebar with tsunami area information.
*
* Includes:
* - Clearing previous plotted data
* - Fetching tsunami areas from a geojson file
* - Plotting tsunami areas on the map
* - Bounding the map to the plotted areas
* - Updating the sidebar with tsunami area information
*
* @param {object} data The tsunami data to render.
* @throws {error} Throws if the tsunami areas geojson cannot be fetched or parsed.
* @returns {promise<void>} Returns a promise that resolves when the tsunami data is rendered.
*/
export async function renderTS(data) {
if (data.cancelled || data === "[]") {
clearAllTsAssets();
return;
}
playSound("tsReport", 0.5);
clearTsunamiLayers();
try {
const response = await fetch("/assets/comparision/tsunami_areas.geojson");
if (!response.ok) {
console.error("[ts/renderTS] failed to fetch tsunami areas geojson");
throw new Error(
`[ts/renderTS] failed to fetch tsunami areas: ${response.status} ${response.statusText}`
);
}
const tsunamiAreasGeoJSON = await response.json();
const areaNameMap = new Map();
tsunamiAreasGeoJSON.features.forEach((feature) => {
const name = feature.properties.name;
areaNameMap.set(name, feature);
});
const matchedFeatures = [];
const bounds = new mapboxgl.LngLatBounds();
if (data.areas && Array.isArray(data.areas)) {
for (const area of data.areas) {
const areaName = area.name;
const grade = area.grade;
const geoJSONFeature = areaNameMap.get(areaName);
if (geoJSONFeature) {
const feature = {
...geoJSONFeature,
properties: {
...geoJSONFeature.properties,
grade: grade,
maxHeight: area.maxHeight?.description || "Unknown",
firstHeight: area.firstHeight?.condition || "Unknown",
},
};
matchedFeatures.push(feature);
if (geoJSONFeature.geometry.type === "LineString") {
geoJSONFeature.geometry.coordinates.forEach((coord) => {
bounds.extend(coord);
});
} else if (geoJSONFeature.geometry.type === "MultiLineString") {
geoJSONFeature.geometry.coordinates.forEach((line) => {
line.forEach((coord) => {
bounds.extend(coord);
});
});
}
} else {
console.warn(
`[ts/renderTS] area given not found in geojson: ${areaName}`
);
}
}
}
if (matchedFeatures.length === 0) {
console.warn("[ts/renderTS] no matching areas found in geojson");
currentTsunamiBounds = null;
disarmTsComponent();
return;
}
map.addSource("tsunamiAreas", {
type: "geojson",
tolerance: 0,
data: {
type: "FeatureCollection",
features: matchedFeatures,
},
});
map.addLayer({
id: "tsunamiAreas",
type: "line",
source: "tsunamiAreas",
paint: {
"line-color": [
"case",
["==", ["get", "grade"], "Watch"],
"#ffff00",
["==", ["get", "grade"], "Warning"],
"#ff0000",
["==", ["get", "grade"], "MajorWarning"],
"#ff00ff",
"#707070",
],
"line-width": 2,
"line-opacity": 1,
"line-emissive-strength": 1,
},
});
if (tsunamiFlashInterval) {
clearInterval(tsunamiFlashInterval);
tsunamiFlashInterval = null;
}
if (tsunamiFlashTimeout) {
clearTimeout(tsunamiFlashTimeout);
tsunamiFlashTimeout = null;
}
function setTsunamiLayerVisibility(vis) {
if (map.getLayer("tsunamiAreas")) {
map.setLayoutProperty(
"tsunamiAreas",
"visibility",
vis ? "visible" : "none"
);
}
}
setTsunamiLayerVisibility(true);
tsunamiFlashInterval = setInterval(() => {
setTsunamiLayerVisibility(false);
tsunamiFlashTimeout = setTimeout(() => {
setTsunamiLayerVisibility(true);
}, 500);
}, 1500);
const highestGrade = Math.max(
...matchedFeatures.map((f) => {
const grade = f.properties.grade;
switch (grade) {
case "MajorWarning":
return 3;
case "Warning":
return 2;
case "Watch":
return 1;
default:
return 0;
}
})
);
let gradeText = "Watch";
if (highestGrade === 3) gradeText = "Major Warning";
else if (highestGrade === 2) gradeText = "Warning";
if (!bounds.isEmpty()) {
currentTsunamiBounds = bounds;
internalBound(bounds);
} else {
currentTsunamiBounds = null;
}
console.info(
`[ts/renderTS] job rendered ${matchedFeatures.length} tsunami areas`
);
[
"tsunami-major-warning-list",
"tsunami-warning-list",
"tsunami-watch-list",
].forEach((id) => {
if (!document.getElementById(id)) {
const h3 = Array.from(document.querySelectorAll("#sidebar h3")).find(
(h) =>
h.textContent &&
h.textContent.includes(
id.includes("major")
? "Major Warning"
: id.includes("warning")
? "Warning"
: "Watch"
)
);
if (
h3 &&
h3.parentElement &&
!h3.parentElement.nextElementSibling?.querySelector(`#${id}`)
) {
const sect = h3.parentElement.parentElement;
const div = document.createElement("div");
div.id = id;
div.className = "space-y-1";
sect.appendChild(div);
}
}
});
updateTsunamiSidebar(data.areas || [], tsunamiAreasGeoJSON.features);
armTsComponent();
} catch (error) {
console.error(
"[ts/forecastComponent] error rendering tsunami data: ",
error
);
disarmTsComponent();
}
}
/**
* Arms the tsunami component by making the tsunami information container visible.
* This function is called when tsunami data is available and needs to be displayed.
*
* @returns {void}
* @private
*/
function armTsComponent() {
document.getElementById("tsInfoContainer").classList.remove("hidden");
document.getElementById("tsunamiContainer").classList.remove("hidden");
document.getElementById("noInfoIssuedText").classList.add("hidden");
}
/**
* Disarms the tsunami component by hiding the tsunami information container.
* This function is called when there is no tsunami data to display.
*
* @returns {void}
* @private
*/
function disarmTsComponent() {
document.getElementById("tsInfoContainer").classList.add("hidden");
document.getElementById("tsunamiContainer").classList.add("hidden");
document.getElementById("noInfoIssuedText").classList.remove("hidden");
}