import mapboxgl from "mapbox-gl";
import {
intDetailSubtitleSelector,
updateInfoBox,
} from "../../../components/infoBox/infoBoxController.js";
import { map } from "../../initMap.js";
import clear551 from "../../internal/clear551.js";
import { internalBound } from "../../internal/internalBound.js";
import playSound from "../../../sound/playSound.js";
import {
armIntList,
updateIntList,
} from "../../../components/infoBox/updateIntList.js";
/**
* Helper function to update the epicenter icon on the map.
*
* @param {*} epicenterLng Longitude of the epicenter
* @param {*} epicenterLat Latitude of the epicenter
*/
export async function updateEpicenterIcon(epicenterLng, epicenterLat) {
if (!map.hasImage("oldEpicenter")) {
await new Promise((resolve, reject) => {
map.loadImage(
"/assets/basemap/icons/oldEpicenter.png",
(error, image) => {
if (error) {
reject(error);
return;
}
map.addImage("oldEpicenter", image);
resolve();
},
);
});
}
map.addSource("epicenterIcon", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [epicenterLng, epicenterLat],
},
},
],
},
});
map.addLayer({
id: "epicenterIcon",
type: "symbol",
source: "epicenterIcon",
layout: {
"icon-image": "oldEpicenter",
"icon-size": 30 / 31, // USAGE: mapIconSizePX / imageSizePX
},
});
}
/**
* Iterate and plots the stations with it's intensity on the map.
*
* @param {*} data Data containing station information.
* @returns {Promise<Array>} Returns a promise that resolves to an array of station coordinates.
*/
export async function plotStations(data) {
if (map.getLayer("stationsLayer")) {
map.removeLayer("stationsLayer");
}
if (map.getSource("stationsLayer")) {
map.removeSource("stationsLayer");
}
try {
const response = await fetch("/assets/comparision/stationRef.csv");
if (!response.ok) {
console.error("[ds/plotStations] bad stationRef data");
throw new Error(
`[ds/plotStations] failed to fetch stationRef.csv: ${response.status} ${response.statusText}`,
);
}
const csvText = await response.text();
const stationMap = new Map();
const lines = csvText.trim().split("\n");
for (let i = 0; i < lines.length; i++) {
const [name, , , lat, long] = lines[i].split(",");
stationMap.set(name, { lat: parseFloat(lat), long: parseFloat(long) });
}
const features = [];
const iconPromises = [];
const loadedIcons = new Set();
const stationCoordinates = [];
const scaleValues = new Set(data.points.map((point) => point.scale));
for (const scale of scaleValues) {
const iconName = `intensity-${scale}`;
if (!map.hasImage(iconName) && !loadedIcons.has(iconName)) {
loadedIcons.add(iconName);
const iconPromise = new Promise((resolve, reject) => {
map.loadImage(
`/assets/basemap/icons/intensities/${scale}.png`,
(error, image) => {
if (error) {
console.warn(
`[ds/plotStations] bad scale image: ${scale}, `,
error,
" using fallback",
);
map.loadImage(
"/assets/basemap/icons/intensities/invalid.png",
(fallbackError, fallbackImage) => {
if (fallbackError) {
console.error(
`[ds/plotStations] failed to load fallback icon: ${iconName} `,
fallbackError,
);
reject(fallbackError);
} else {
map.addImage(iconName, fallbackImage);
resolve();
}
},
);
} else {
map.addImage(iconName, image);
resolve();
}
},
);
});
iconPromises.push(iconPromise);
}
}
await Promise.all(iconPromises);
for (const point of data.points) {
const stationInfo = stationMap.get(point.addr);
if (stationInfo) {
features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: [stationInfo.long, stationInfo.lat],
},
properties: {
scale: point.scale,
name: point.addr,
pref: point.pref,
},
});
stationCoordinates.push([stationInfo.long, stationInfo.lat]);
} else {
console.warn(
`[ds/plotStations] station not found in ref data: ${point.addr}`,
);
}
}
map.addSource("stationsLayer", {
type: "geojson",
data: {
type: "FeatureCollection",
features: features,
},
});
map.addLayer(
{
id: "stationsLayer",
type: "symbol",
source: "stationsLayer",
layout: {
"icon-image": [
"concat",
"intensity-",
["to-string", ["get", "scale"]],
],
"icon-size": 20 / 30, // USAGE: mapIconSizePX / imageSizePX
"icon-allow-overlap": true,
},
paint: {
"text-color": "#000000",
"text-halo-color": "#ffffff",
"text-halo-width": 1,
"symbol-sort": ["get", "scale"],
},
},
"epicenterIcon",
);
return stationCoordinates;
} catch (error) {
console.error("[ds/plotStations] error plotting stations: ", error);
return [];
}
}
/**
* Bounds the map view to the epicenter and stations.
*
* @param {*} epicenter The epicenter coordinates.
* @param {*} stationCoordinates The coordinates of the stations.
*/
export async function boundMarkers(epicenter, stationCoordinates) {
const bounds = new mapboxgl.LngLatBounds();
bounds.extend([epicenter.longitude, epicenter.latitude]);
if (stationCoordinates && stationCoordinates.length > 0) {
for (const [long, lat] of stationCoordinates) {
bounds.extend([long, lat]);
}
} else {
console.warn(
"[ds] no station coordinates available for bounding, using epicenter only",
);
}
internalBound(bounds);
}
/**
* A part of the main rendering logic for DetailScale (DS) on response code 551.
*
* Renders the DetailScale data on the map and updates the information box and sidebar.
*
* Includes:
* - Clearing previous plotted data
* - Epicenter icon update
* - Station plotting with intensity
* - Map bounding to epicenter and stations
* - Info box update with detailed epicenter information
*
* @param {Object} data The DetailScale data to render.
* @returns {Promise<void>} Returns a promise that resolves when the DetailScale is rendered.
* @throws {Error} Throws an error if the stationRef.csv cannot be fetched or parsed.
*/
export async function renderDS(data) {
playSound("detailScale", 0.5);
clear551();
armIntList();
const hyp = data.earthquake.hypocenter;
updateInfoBox(
"Detailed Epicenter Information",
hyp.name,
hyp.magnitude,
hyp.depth,
data.earthquake.time,
"",
data.earthquake.maxScale,
);
const epicenterLat = hyp.latitude;
const epicenterLng = hyp.longitude;
await updateEpicenterIcon(epicenterLng, epicenterLat);
const stationCoordinates = await plotStations(data);
await boundMarkers(data.earthquake.hypocenter, stationCoordinates);
const stationMap = await getStationMap();
await updateIntList(data, stationMap);
intDetailSubtitleSelector(data.issue.type);
console.info("[ds] renderDS completed");
}
async function getStationMap() {
const response = await fetch("/assets/comparision/stationRef.csv");
if (!response.ok) {
throw new Error(`Failed to fetch stationRef.csv: ${response.status}`);
}
const csvText = await response.text();
const stationMap = new Map();
const lines = csvText.trim().split("\n");
for (let i = 0; i < lines.length; i++) {
const [name, , , lat, long] = lines[i].split(",");
stationMap.set(name, { lat: parseFloat(lat), long: parseFloat(long) });
}
return stationMap;
}