refine hazard history matching and hazard list SQL selection
Some checks failed
build-docker / Build Image (push) Has been cancelled
Some checks failed
build-docker / Build Image (push) Has been cancelled
This commit is contained in:
parent
82885004c6
commit
0f2d64b908
1 changed files with 135 additions and 31 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { getPool } from './mysql.mjs';
|
||||
|
||||
const MAX_HISTORY_ENTRIES = 7;
|
||||
const PRACTICAL_LOCATION_RADIUS_KM = 50;
|
||||
|
||||
const toIsoString = (value) => {
|
||||
if (!value) return null;
|
||||
|
|
@ -20,6 +21,60 @@ const mapRowToHistoryEntry = (row) => ({
|
|||
ongoing: Boolean(row.ongoing),
|
||||
});
|
||||
|
||||
const parseLocationKey = (locationKey) => {
|
||||
if (typeof locationKey !== 'string') return null;
|
||||
const [latText, lonText] = locationKey.split(',');
|
||||
const lat = Number.parseFloat(latText);
|
||||
const lon = Number.parseFloat(lonText);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
||||
return { lat, lon };
|
||||
};
|
||||
|
||||
const distanceKm = (a, b) => {
|
||||
const toRadians = (value) => value * (Math.PI / 180);
|
||||
const earthRadiusKm = 6371;
|
||||
const dLat = toRadians(b.lat - a.lat);
|
||||
const dLon = toRadians(b.lon - a.lon);
|
||||
const lat1 = toRadians(a.lat);
|
||||
const lat2 = toRadians(b.lat);
|
||||
const haversine = Math.sin(dLat / 2) ** 2
|
||||
+ Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * earthRadiusKm * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine));
|
||||
};
|
||||
|
||||
const isSamePracticalLocation = (row, location, locationKey) => {
|
||||
if (row.location_key === locationKey) return true;
|
||||
if (row.location_label !== location) return false;
|
||||
const rowCoords = parseLocationKey(row.location_key);
|
||||
const currentCoords = parseLocationKey(locationKey);
|
||||
if (!rowCoords || !currentCoords) return false;
|
||||
return distanceKm(rowCoords, currentCoords) <= PRACTICAL_LOCATION_RADIUS_KM;
|
||||
};
|
||||
|
||||
const getCandidateRows = async (connection, location, locationKey) => {
|
||||
const [rows] = await connection.execute(
|
||||
`SELECT
|
||||
id,
|
||||
location_label,
|
||||
location_key,
|
||||
hazard_type,
|
||||
source,
|
||||
severity,
|
||||
latest_hazard_id,
|
||||
encountered_at,
|
||||
last_seen_at,
|
||||
ongoing
|
||||
FROM hazard_history
|
||||
WHERE location_key = ?
|
||||
OR location_label = ?`,
|
||||
[locationKey, location],
|
||||
);
|
||||
|
||||
return rows.filter((row) => isSamePracticalLocation(row, location, locationKey));
|
||||
};
|
||||
|
||||
const buildHazardIdentity = (hazardType, source) => `${hazardType}::${source}`;
|
||||
|
||||
/**
|
||||
* Format location label from weather parameters
|
||||
* @param {string} city - City name
|
||||
|
|
@ -43,17 +98,34 @@ const formatLocation = (city, state, country, countryCode) => {
|
|||
const getHistory = async () => {
|
||||
const [rows] = await getPool().query(
|
||||
`SELECT
|
||||
location_label,
|
||||
location_key,
|
||||
hazard_type,
|
||||
source,
|
||||
severity,
|
||||
latest_hazard_id,
|
||||
encountered_at,
|
||||
last_seen_at,
|
||||
ongoing
|
||||
FROM hazard_history
|
||||
ORDER BY last_seen_at DESC
|
||||
h.location_label,
|
||||
h.location_key,
|
||||
h.hazard_type,
|
||||
h.source,
|
||||
h.severity,
|
||||
h.latest_hazard_id,
|
||||
h.encountered_at,
|
||||
h.last_seen_at,
|
||||
h.ongoing
|
||||
FROM hazard_history h
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM hazard_history h2
|
||||
WHERE h2.location_key = h.location_key
|
||||
AND (
|
||||
h2.last_seen_at > h.last_seen_at
|
||||
OR (
|
||||
h2.last_seen_at = h.last_seen_at
|
||||
AND h2.ongoing > h.ongoing
|
||||
)
|
||||
OR (
|
||||
h2.last_seen_at = h.last_seen_at
|
||||
AND h2.ongoing = h.ongoing
|
||||
AND h2.id > h.id
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY h.last_seen_at DESC, h.ongoing DESC, h.id DESC
|
||||
LIMIT ?`,
|
||||
[MAX_HISTORY_ENTRIES],
|
||||
);
|
||||
|
|
@ -69,32 +141,64 @@ const updateHistory = async (payload) => {
|
|||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
const candidateRows = await getCandidateRows(connection, location, locationKey);
|
||||
const activeHazardKeys = new Set(validHazards.map((hazard) => buildHazardIdentity(hazard.hazardType, hazard.source)));
|
||||
|
||||
if (validHazards.length === 0) {
|
||||
await connection.execute(
|
||||
`UPDATE hazard_history
|
||||
SET ongoing = 0,
|
||||
last_seen_at = UTC_TIMESTAMP()
|
||||
WHERE location_key = ?
|
||||
AND ongoing = 1`,
|
||||
[locationKey],
|
||||
);
|
||||
const idsToEnd = candidateRows.filter((row) => row.ongoing).map((row) => row.id);
|
||||
if (idsToEnd.length > 0) {
|
||||
const placeholders = idsToEnd.map(() => '?').join(', ');
|
||||
await connection.execute(
|
||||
`UPDATE hazard_history
|
||||
SET ongoing = 0,
|
||||
last_seen_at = UTC_TIMESTAMP()
|
||||
WHERE id IN (${placeholders})
|
||||
AND ongoing = 1`,
|
||||
idsToEnd,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const keepClauses = validHazards.map(() => '(hazard_type = ? AND source = ?)').join(' OR ');
|
||||
const keepParams = validHazards.flatMap((hazard) => [hazard.hazardType, hazard.source]);
|
||||
|
||||
await connection.execute(
|
||||
`UPDATE hazard_history
|
||||
SET ongoing = 0,
|
||||
last_seen_at = UTC_TIMESTAMP()
|
||||
WHERE location_key = ?
|
||||
AND ongoing = 1
|
||||
AND NOT (${keepClauses})`,
|
||||
[locationKey, ...keepParams],
|
||||
);
|
||||
const idsToEnd = candidateRows
|
||||
.filter((row) => row.ongoing && !activeHazardKeys.has(buildHazardIdentity(row.hazard_type, row.source)))
|
||||
.map((row) => row.id);
|
||||
if (idsToEnd.length > 0) {
|
||||
const placeholders = idsToEnd.map(() => '?').join(', ');
|
||||
await connection.execute(
|
||||
`UPDATE hazard_history
|
||||
SET ongoing = 0,
|
||||
last_seen_at = UTC_TIMESTAMP()
|
||||
WHERE id IN (${placeholders})
|
||||
AND ongoing = 1`,
|
||||
idsToEnd,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const hazard of validHazards) {
|
||||
const nearbyMatch = candidateRows.find((row) => row.hazard_type === hazard.hazardType
|
||||
&& row.source === hazard.source
|
||||
&& row.location_key !== locationKey);
|
||||
|
||||
if (nearbyMatch) {
|
||||
await connection.execute(
|
||||
`UPDATE hazard_history
|
||||
SET location_key = ?,
|
||||
location_label = ?,
|
||||
severity = ?,
|
||||
latest_hazard_id = ?,
|
||||
last_seen_at = UTC_TIMESTAMP(),
|
||||
ongoing = 1
|
||||
WHERE id = ?`,
|
||||
[
|
||||
locationKey,
|
||||
location,
|
||||
hazard.severity ?? null,
|
||||
hazard.id ?? null,
|
||||
nearbyMatch.id,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await connection.execute(
|
||||
`INSERT INTO hazard_history (
|
||||
location_label,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue