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';
|
import { getPool } from './mysql.mjs';
|
||||||
|
|
||||||
const MAX_HISTORY_ENTRIES = 7;
|
const MAX_HISTORY_ENTRIES = 7;
|
||||||
|
const PRACTICAL_LOCATION_RADIUS_KM = 50;
|
||||||
|
|
||||||
const toIsoString = (value) => {
|
const toIsoString = (value) => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|
@ -20,6 +21,60 @@ const mapRowToHistoryEntry = (row) => ({
|
||||||
ongoing: Boolean(row.ongoing),
|
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
|
* Format location label from weather parameters
|
||||||
* @param {string} city - City name
|
* @param {string} city - City name
|
||||||
|
|
@ -43,17 +98,34 @@ const formatLocation = (city, state, country, countryCode) => {
|
||||||
const getHistory = async () => {
|
const getHistory = async () => {
|
||||||
const [rows] = await getPool().query(
|
const [rows] = await getPool().query(
|
||||||
`SELECT
|
`SELECT
|
||||||
location_label,
|
h.location_label,
|
||||||
location_key,
|
h.location_key,
|
||||||
hazard_type,
|
h.hazard_type,
|
||||||
source,
|
h.source,
|
||||||
severity,
|
h.severity,
|
||||||
latest_hazard_id,
|
h.latest_hazard_id,
|
||||||
encountered_at,
|
h.encountered_at,
|
||||||
last_seen_at,
|
h.last_seen_at,
|
||||||
ongoing
|
h.ongoing
|
||||||
FROM hazard_history
|
FROM hazard_history h
|
||||||
ORDER BY last_seen_at DESC
|
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 ?`,
|
LIMIT ?`,
|
||||||
[MAX_HISTORY_ENTRIES],
|
[MAX_HISTORY_ENTRIES],
|
||||||
);
|
);
|
||||||
|
|
@ -69,32 +141,64 @@ const updateHistory = async (payload) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
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) {
|
if (validHazards.length === 0) {
|
||||||
await connection.execute(
|
const idsToEnd = candidateRows.filter((row) => row.ongoing).map((row) => row.id);
|
||||||
`UPDATE hazard_history
|
if (idsToEnd.length > 0) {
|
||||||
SET ongoing = 0,
|
const placeholders = idsToEnd.map(() => '?').join(', ');
|
||||||
last_seen_at = UTC_TIMESTAMP()
|
await connection.execute(
|
||||||
WHERE location_key = ?
|
`UPDATE hazard_history
|
||||||
AND ongoing = 1`,
|
SET ongoing = 0,
|
||||||
[locationKey],
|
last_seen_at = UTC_TIMESTAMP()
|
||||||
);
|
WHERE id IN (${placeholders})
|
||||||
|
AND ongoing = 1`,
|
||||||
|
idsToEnd,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const keepClauses = validHazards.map(() => '(hazard_type = ? AND source = ?)').join(' OR ');
|
const idsToEnd = candidateRows
|
||||||
const keepParams = validHazards.flatMap((hazard) => [hazard.hazardType, hazard.source]);
|
.filter((row) => row.ongoing && !activeHazardKeys.has(buildHazardIdentity(row.hazard_type, row.source)))
|
||||||
|
.map((row) => row.id);
|
||||||
await connection.execute(
|
if (idsToEnd.length > 0) {
|
||||||
`UPDATE hazard_history
|
const placeholders = idsToEnd.map(() => '?').join(', ');
|
||||||
SET ongoing = 0,
|
await connection.execute(
|
||||||
last_seen_at = UTC_TIMESTAMP()
|
`UPDATE hazard_history
|
||||||
WHERE location_key = ?
|
SET ongoing = 0,
|
||||||
AND ongoing = 1
|
last_seen_at = UTC_TIMESTAMP()
|
||||||
AND NOT (${keepClauses})`,
|
WHERE id IN (${placeholders})
|
||||||
[locationKey, ...keepParams],
|
AND ongoing = 1`,
|
||||||
);
|
idsToEnd,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const hazard of validHazards) {
|
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(
|
await connection.execute(
|
||||||
`INSERT INTO hazard_history (
|
`INSERT INTO hazard_history (
|
||||||
location_label,
|
location_label,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue