local-risk-alert-feed 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/adapters/index.js +8 -0
- package/dist/cjs/adapters/index.js.map +1 -0
- package/dist/cjs/adapters/lambda.js +143 -0
- package/dist/cjs/adapters/lambda.js.map +1 -0
- package/dist/cjs/adapters/vercel.js +119 -0
- package/dist/cjs/adapters/vercel.js.map +1 -0
- package/dist/cjs/core/alert-aggregator.js +207 -0
- package/dist/cjs/core/alert-aggregator.js.map +1 -0
- package/dist/cjs/core/alert-feed.js +236 -0
- package/dist/cjs/core/alert-feed.js.map +1 -0
- package/dist/cjs/core/index.js +22 -0
- package/dist/cjs/core/index.js.map +1 -0
- package/dist/cjs/core/plugin-registry.js +193 -0
- package/dist/cjs/core/plugin-registry.js.map +1 -0
- package/dist/cjs/core/plugin-resolver.js +121 -0
- package/dist/cjs/core/plugin-resolver.js.map +1 -0
- package/dist/cjs/core/time-range.js +67 -0
- package/dist/cjs/core/time-range.js.map +1 -0
- package/dist/cjs/errors/fetch-error.js +71 -0
- package/dist/cjs/errors/fetch-error.js.map +1 -0
- package/dist/cjs/errors/index.js +15 -0
- package/dist/cjs/errors/index.js.map +1 -0
- package/dist/cjs/errors/plugin-error.js +80 -0
- package/dist/cjs/errors/plugin-error.js.map +1 -0
- package/dist/cjs/errors/validation-error.js +49 -0
- package/dist/cjs/errors/validation-error.js.map +1 -0
- package/dist/cjs/geo/distance.js +94 -0
- package/dist/cjs/geo/distance.js.map +1 -0
- package/dist/cjs/geo/index.js +18 -0
- package/dist/cjs/geo/index.js.map +1 -0
- package/dist/cjs/geo/point-in-radius.js +86 -0
- package/dist/cjs/geo/point-in-radius.js.map +1 -0
- package/dist/cjs/index.js +90 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/plugins/air-quality/airnow.plugin.js +343 -0
- package/dist/cjs/plugins/air-quality/airnow.plugin.js.map +1 -0
- package/dist/cjs/plugins/air-quality/index.js +6 -0
- package/dist/cjs/plugins/air-quality/index.js.map +1 -0
- package/dist/cjs/plugins/base-plugin.js +213 -0
- package/dist/cjs/plugins/base-plugin.js.map +1 -0
- package/dist/cjs/plugins/events/index.js +6 -0
- package/dist/cjs/plugins/events/index.js.map +1 -0
- package/dist/cjs/plugins/events/phoenix-events.plugin.js +382 -0
- package/dist/cjs/plugins/events/phoenix-events.plugin.js.map +1 -0
- package/dist/cjs/plugins/fire-emt/index.js +6 -0
- package/dist/cjs/plugins/fire-emt/index.js.map +1 -0
- package/dist/cjs/plugins/fire-emt/phoenix-fire.plugin.js +262 -0
- package/dist/cjs/plugins/fire-emt/phoenix-fire.plugin.js.map +1 -0
- package/dist/cjs/plugins/index.js +28 -0
- package/dist/cjs/plugins/index.js.map +1 -0
- package/dist/cjs/plugins/police-blotter/index.js +6 -0
- package/dist/cjs/plugins/police-blotter/index.js.map +1 -0
- package/dist/cjs/plugins/police-blotter/phoenix-police.plugin.js +198 -0
- package/dist/cjs/plugins/police-blotter/phoenix-police.plugin.js.map +1 -0
- package/dist/cjs/plugins/pulsepoint/index.js +6 -0
- package/dist/cjs/plugins/pulsepoint/index.js.map +1 -0
- package/dist/cjs/plugins/pulsepoint/pulsepoint.plugin.js +275 -0
- package/dist/cjs/plugins/pulsepoint/pulsepoint.plugin.js.map +1 -0
- package/dist/cjs/plugins/traffic/arizona-traffic.plugin.js +391 -0
- package/dist/cjs/plugins/traffic/arizona-traffic.plugin.js.map +1 -0
- package/dist/cjs/plugins/traffic/index.js +6 -0
- package/dist/cjs/plugins/traffic/index.js.map +1 -0
- package/dist/cjs/plugins/weather/index.js +6 -0
- package/dist/cjs/plugins/weather/index.js.map +1 -0
- package/dist/cjs/plugins/weather/nws-weather.plugin.js +180 -0
- package/dist/cjs/plugins/weather/nws-weather.plugin.js.map +1 -0
- package/dist/cjs/schemas/alert.schema.js +93 -0
- package/dist/cjs/schemas/alert.schema.js.map +1 -0
- package/dist/cjs/schemas/index.js +24 -0
- package/dist/cjs/schemas/index.js.map +1 -0
- package/dist/cjs/schemas/query.schema.js +76 -0
- package/dist/cjs/schemas/query.schema.js.map +1 -0
- package/dist/cjs/types/alert.js +35 -0
- package/dist/cjs/types/alert.js.map +1 -0
- package/dist/cjs/types/config.js +13 -0
- package/dist/cjs/types/config.js.map +1 -0
- package/dist/cjs/types/geo.js +3 -0
- package/dist/cjs/types/geo.js.map +1 -0
- package/dist/cjs/types/index.js +16 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/types/plugin.js +3 -0
- package/dist/cjs/types/plugin.js.map +1 -0
- package/dist/cjs/types/query.js +28 -0
- package/dist/cjs/types/query.js.map +1 -0
- package/dist/cjs/utils/cache.js +188 -0
- package/dist/cjs/utils/cache.js.map +1 -0
- package/dist/cjs/utils/csv.js +189 -0
- package/dist/cjs/utils/csv.js.map +1 -0
- package/dist/cjs/utils/date.js +153 -0
- package/dist/cjs/utils/date.js.map +1 -0
- package/dist/cjs/utils/index.js +28 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/cjs/utils/retry.js +109 -0
- package/dist/cjs/utils/retry.js.map +1 -0
- package/dist/esm/adapters/index.js +3 -0
- package/dist/esm/adapters/index.js.map +1 -0
- package/dist/esm/adapters/lambda.js +140 -0
- package/dist/esm/adapters/lambda.js.map +1 -0
- package/dist/esm/adapters/vercel.js +116 -0
- package/dist/esm/adapters/vercel.js.map +1 -0
- package/dist/esm/core/alert-aggregator.js +203 -0
- package/dist/esm/core/alert-aggregator.js.map +1 -0
- package/dist/esm/core/alert-feed.js +232 -0
- package/dist/esm/core/alert-feed.js.map +1 -0
- package/dist/esm/core/index.js +6 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/plugin-registry.js +189 -0
- package/dist/esm/core/plugin-registry.js.map +1 -0
- package/dist/esm/core/plugin-resolver.js +117 -0
- package/dist/esm/core/plugin-resolver.js.map +1 -0
- package/dist/esm/core/time-range.js +57 -0
- package/dist/esm/core/time-range.js.map +1 -0
- package/dist/esm/errors/fetch-error.js +67 -0
- package/dist/esm/errors/fetch-error.js.map +1 -0
- package/dist/esm/errors/index.js +4 -0
- package/dist/esm/errors/index.js.map +1 -0
- package/dist/esm/errors/plugin-error.js +71 -0
- package/dist/esm/errors/plugin-error.js.map +1 -0
- package/dist/esm/errors/validation-error.js +45 -0
- package/dist/esm/errors/validation-error.js.map +1 -0
- package/dist/esm/geo/distance.js +85 -0
- package/dist/esm/geo/distance.js.map +1 -0
- package/dist/esm/geo/index.js +3 -0
- package/dist/esm/geo/index.js.map +1 -0
- package/dist/esm/geo/point-in-radius.js +79 -0
- package/dist/esm/geo/point-in-radius.js.map +1 -0
- package/dist/esm/index.js +30 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugins/air-quality/airnow.plugin.js +339 -0
- package/dist/esm/plugins/air-quality/airnow.plugin.js.map +1 -0
- package/dist/esm/plugins/air-quality/index.js +2 -0
- package/dist/esm/plugins/air-quality/index.js.map +1 -0
- package/dist/esm/plugins/base-plugin.js +209 -0
- package/dist/esm/plugins/base-plugin.js.map +1 -0
- package/dist/esm/plugins/events/index.js +2 -0
- package/dist/esm/plugins/events/index.js.map +1 -0
- package/dist/esm/plugins/events/phoenix-events.plugin.js +378 -0
- package/dist/esm/plugins/events/phoenix-events.plugin.js.map +1 -0
- package/dist/esm/plugins/fire-emt/index.js +2 -0
- package/dist/esm/plugins/fire-emt/index.js.map +1 -0
- package/dist/esm/plugins/fire-emt/phoenix-fire.plugin.js +258 -0
- package/dist/esm/plugins/fire-emt/phoenix-fire.plugin.js.map +1 -0
- package/dist/esm/plugins/index.js +17 -0
- package/dist/esm/plugins/index.js.map +1 -0
- package/dist/esm/plugins/police-blotter/index.js +2 -0
- package/dist/esm/plugins/police-blotter/index.js.map +1 -0
- package/dist/esm/plugins/police-blotter/phoenix-police.plugin.js +194 -0
- package/dist/esm/plugins/police-blotter/phoenix-police.plugin.js.map +1 -0
- package/dist/esm/plugins/pulsepoint/index.js +2 -0
- package/dist/esm/plugins/pulsepoint/index.js.map +1 -0
- package/dist/esm/plugins/pulsepoint/pulsepoint.plugin.js +271 -0
- package/dist/esm/plugins/pulsepoint/pulsepoint.plugin.js.map +1 -0
- package/dist/esm/plugins/traffic/arizona-traffic.plugin.js +387 -0
- package/dist/esm/plugins/traffic/arizona-traffic.plugin.js.map +1 -0
- package/dist/esm/plugins/traffic/index.js +2 -0
- package/dist/esm/plugins/traffic/index.js.map +1 -0
- package/dist/esm/plugins/weather/index.js +2 -0
- package/dist/esm/plugins/weather/index.js.map +1 -0
- package/dist/esm/plugins/weather/nws-weather.plugin.js +176 -0
- package/dist/esm/plugins/weather/nws-weather.plugin.js.map +1 -0
- package/dist/esm/schemas/alert.schema.js +90 -0
- package/dist/esm/schemas/alert.schema.js.map +1 -0
- package/dist/esm/schemas/index.js +5 -0
- package/dist/esm/schemas/index.js.map +1 -0
- package/dist/esm/schemas/query.schema.js +72 -0
- package/dist/esm/schemas/query.schema.js.map +1 -0
- package/dist/esm/types/alert.js +32 -0
- package/dist/esm/types/alert.js.map +1 -0
- package/dist/esm/types/config.js +10 -0
- package/dist/esm/types/config.js.map +1 -0
- package/dist/esm/types/geo.js +2 -0
- package/dist/esm/types/geo.js.map +1 -0
- package/dist/esm/types/index.js +4 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/types/plugin.js +2 -0
- package/dist/esm/types/plugin.js.map +1 -0
- package/dist/esm/types/query.js +25 -0
- package/dist/esm/types/query.js.map +1 -0
- package/dist/esm/utils/cache.js +181 -0
- package/dist/esm/utils/cache.js.map +1 -0
- package/dist/esm/utils/csv.js +185 -0
- package/dist/esm/utils/csv.js.map +1 -0
- package/dist/esm/utils/date.js +142 -0
- package/dist/esm/utils/date.js.map +1 -0
- package/dist/esm/utils/index.js +5 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/dist/esm/utils/retry.js +102 -0
- package/dist/esm/utils/retry.js.map +1 -0
- package/dist/types/adapters/index.d.ts +5 -0
- package/dist/types/adapters/index.d.ts.map +1 -0
- package/dist/types/adapters/lambda.d.ts +37 -0
- package/dist/types/adapters/lambda.d.ts.map +1 -0
- package/dist/types/adapters/vercel.d.ts +54 -0
- package/dist/types/adapters/vercel.d.ts.map +1 -0
- package/dist/types/core/alert-aggregator.d.ts +81 -0
- package/dist/types/core/alert-aggregator.d.ts.map +1 -0
- package/dist/types/core/alert-feed.d.ts +80 -0
- package/dist/types/core/alert-feed.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +8 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/plugin-registry.d.ts +91 -0
- package/dist/types/core/plugin-registry.d.ts.map +1 -0
- package/dist/types/core/plugin-resolver.d.ts +78 -0
- package/dist/types/core/plugin-resolver.d.ts.map +1 -0
- package/dist/types/core/time-range.d.ts +40 -0
- package/dist/types/core/time-range.d.ts.map +1 -0
- package/dist/types/errors/fetch-error.d.ts +46 -0
- package/dist/types/errors/fetch-error.d.ts.map +1 -0
- package/dist/types/errors/index.d.ts +5 -0
- package/dist/types/errors/index.d.ts.map +1 -0
- package/dist/types/errors/plugin-error.d.ts +42 -0
- package/dist/types/errors/plugin-error.d.ts.map +1 -0
- package/dist/types/errors/validation-error.d.ts +34 -0
- package/dist/types/errors/validation-error.d.ts.map +1 -0
- package/dist/types/geo/distance.d.ts +50 -0
- package/dist/types/geo/distance.d.ts.map +1 -0
- package/dist/types/geo/index.d.ts +3 -0
- package/dist/types/geo/index.d.ts.map +1 -0
- package/dist/types/geo/point-in-radius.d.ts +44 -0
- package/dist/types/geo/point-in-radius.d.ts.map +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/plugins/air-quality/airnow.plugin.d.ts +84 -0
- package/dist/types/plugins/air-quality/airnow.plugin.d.ts.map +1 -0
- package/dist/types/plugins/air-quality/index.d.ts +3 -0
- package/dist/types/plugins/air-quality/index.d.ts.map +1 -0
- package/dist/types/plugins/base-plugin.d.ts +99 -0
- package/dist/types/plugins/base-plugin.d.ts.map +1 -0
- package/dist/types/plugins/events/index.d.ts +3 -0
- package/dist/types/plugins/events/index.d.ts.map +1 -0
- package/dist/types/plugins/events/phoenix-events.plugin.d.ts +71 -0
- package/dist/types/plugins/events/phoenix-events.plugin.d.ts.map +1 -0
- package/dist/types/plugins/fire-emt/index.d.ts +3 -0
- package/dist/types/plugins/fire-emt/index.d.ts.map +1 -0
- package/dist/types/plugins/fire-emt/phoenix-fire.plugin.d.ts +47 -0
- package/dist/types/plugins/fire-emt/phoenix-fire.plugin.d.ts.map +1 -0
- package/dist/types/plugins/index.d.ts +17 -0
- package/dist/types/plugins/index.d.ts.map +1 -0
- package/dist/types/plugins/police-blotter/index.d.ts +3 -0
- package/dist/types/plugins/police-blotter/index.d.ts.map +1 -0
- package/dist/types/plugins/police-blotter/phoenix-police.plugin.d.ts +49 -0
- package/dist/types/plugins/police-blotter/phoenix-police.plugin.d.ts.map +1 -0
- package/dist/types/plugins/pulsepoint/index.d.ts +3 -0
- package/dist/types/plugins/pulsepoint/index.d.ts.map +1 -0
- package/dist/types/plugins/pulsepoint/pulsepoint.plugin.d.ts +61 -0
- package/dist/types/plugins/pulsepoint/pulsepoint.plugin.d.ts.map +1 -0
- package/dist/types/plugins/traffic/arizona-traffic.plugin.d.ts +83 -0
- package/dist/types/plugins/traffic/arizona-traffic.plugin.d.ts.map +1 -0
- package/dist/types/plugins/traffic/index.d.ts +3 -0
- package/dist/types/plugins/traffic/index.d.ts.map +1 -0
- package/dist/types/plugins/weather/index.d.ts +3 -0
- package/dist/types/plugins/weather/index.d.ts.map +1 -0
- package/dist/types/plugins/weather/nws-weather.plugin.d.ts +50 -0
- package/dist/types/plugins/weather/nws-weather.plugin.d.ts.map +1 -0
- package/dist/types/schemas/alert.schema.d.ts +266 -0
- package/dist/types/schemas/alert.schema.d.ts.map +1 -0
- package/dist/types/schemas/index.d.ts +5 -0
- package/dist/types/schemas/index.d.ts.map +1 -0
- package/dist/types/schemas/query.schema.d.ts +150 -0
- package/dist/types/schemas/query.schema.d.ts.map +1 -0
- package/dist/types/types/alert.d.ts +96 -0
- package/dist/types/types/alert.d.ts.map +1 -0
- package/dist/types/types/config.d.ts +63 -0
- package/dist/types/types/config.d.ts.map +1 -0
- package/dist/types/types/geo.d.ts +33 -0
- package/dist/types/types/geo.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +9 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/types/plugin.d.ts +125 -0
- package/dist/types/types/plugin.d.ts.map +1 -0
- package/dist/types/types/query.d.ts +86 -0
- package/dist/types/types/query.d.ts.map +1 -0
- package/dist/types/utils/cache.d.ts +112 -0
- package/dist/types/utils/cache.d.ts.map +1 -0
- package/dist/types/utils/csv.d.ts +38 -0
- package/dist/types/utils/csv.d.ts.map +1 -0
- package/dist/types/utils/date.d.ts +47 -0
- package/dist/types/utils/date.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +7 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/dist/types/utils/retry.d.ts +51 -0
- package/dist/types/utils/retry.d.ts.map +1 -0
- package/package.json +115 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { BasePlugin } from '../base-plugin';
|
|
2
|
+
/**
|
|
3
|
+
* Phoenix center coordinates.
|
|
4
|
+
*/
|
|
5
|
+
const PHOENIX_CENTER = {
|
|
6
|
+
latitude: 33.4484,
|
|
7
|
+
longitude: -112.074,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Coverage radius in meters (approximately 50km covers greater Phoenix metro).
|
|
11
|
+
*/
|
|
12
|
+
const COVERAGE_RADIUS_METERS = 50_000;
|
|
13
|
+
/**
|
|
14
|
+
* Default Phoenix metro area agencies.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_PHOENIX_AGENCIES = ['FPHX', 'FMES', 'FTEM', 'FGLN', 'FSCOT'];
|
|
17
|
+
/**
|
|
18
|
+
* Incident type to category and risk mappings.
|
|
19
|
+
*/
|
|
20
|
+
const INCIDENT_TYPE_MAP = {
|
|
21
|
+
// Fire incidents
|
|
22
|
+
'STRUCTURE FIRE': { category: 'fire', risk: 'extreme' },
|
|
23
|
+
'RESIDENTIAL FIRE': { category: 'fire', risk: 'extreme' },
|
|
24
|
+
'COMMERCIAL FIRE': { category: 'fire', risk: 'extreme' },
|
|
25
|
+
'INDUSTRIAL FIRE': { category: 'fire', risk: 'extreme' },
|
|
26
|
+
'APARTMENT FIRE': { category: 'fire', risk: 'extreme' },
|
|
27
|
+
'HIGHRISE FIRE': { category: 'fire', risk: 'extreme' },
|
|
28
|
+
'VEHICLE FIRE': { category: 'fire', risk: 'high' },
|
|
29
|
+
'BRUSH FIRE': { category: 'fire', risk: 'severe' },
|
|
30
|
+
'WILDFIRE': { category: 'fire', risk: 'severe' },
|
|
31
|
+
'GRASS FIRE': { category: 'fire', risk: 'high' },
|
|
32
|
+
'DUMPSTER FIRE': { category: 'fire', risk: 'moderate' },
|
|
33
|
+
'FIRE ALARM': { category: 'fire', risk: 'low' },
|
|
34
|
+
'SMOKE INVESTIGATION': { category: 'fire', risk: 'moderate' },
|
|
35
|
+
// Medical emergencies
|
|
36
|
+
'CARDIAC ARREST': { category: 'medical', risk: 'severe' },
|
|
37
|
+
'CARDIAC EMERGENCY': { category: 'medical', risk: 'severe' },
|
|
38
|
+
'STROKE': { category: 'medical', risk: 'severe' },
|
|
39
|
+
'CHEST PAIN': { category: 'medical', risk: 'high' },
|
|
40
|
+
'DIFFICULTY BREATHING': { category: 'medical', risk: 'high' },
|
|
41
|
+
'RESPIRATORY': { category: 'medical', risk: 'high' },
|
|
42
|
+
'UNCONSCIOUS': { category: 'medical', risk: 'high' },
|
|
43
|
+
'OVERDOSE': { category: 'medical', risk: 'high' },
|
|
44
|
+
'MEDICAL EMERGENCY': { category: 'medical', risk: 'high' },
|
|
45
|
+
'MEDICAL AID': { category: 'medical', risk: 'moderate' },
|
|
46
|
+
'FALL': { category: 'medical', risk: 'moderate' },
|
|
47
|
+
'INJURY': { category: 'medical', risk: 'moderate' },
|
|
48
|
+
'DIABETIC': { category: 'medical', risk: 'high' },
|
|
49
|
+
'SEIZURE': { category: 'medical', risk: 'high' },
|
|
50
|
+
// Traffic/rescue
|
|
51
|
+
'TRAFFIC COLLISION': { category: 'medical', risk: 'high' },
|
|
52
|
+
'TRAFFIC ACCIDENT': { category: 'medical', risk: 'high' },
|
|
53
|
+
'MVA': { category: 'medical', risk: 'high' },
|
|
54
|
+
'EXTRICATION': { category: 'medical', risk: 'severe' },
|
|
55
|
+
'RESCUE': { category: 'medical', risk: 'high' },
|
|
56
|
+
'WATER RESCUE': { category: 'medical', risk: 'severe' },
|
|
57
|
+
'SWIFT WATER RESCUE': { category: 'medical', risk: 'severe' },
|
|
58
|
+
// Hazmat
|
|
59
|
+
'HAZMAT': { category: 'fire', risk: 'severe' },
|
|
60
|
+
'GAS LEAK': { category: 'fire', risk: 'high' },
|
|
61
|
+
'NATURAL GAS LEAK': { category: 'fire', risk: 'high' },
|
|
62
|
+
'CARBON MONOXIDE': { category: 'fire', risk: 'high' },
|
|
63
|
+
'ELECTRICAL': { category: 'fire', risk: 'high' },
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Plugin that fetches real-time fire and EMS incidents from Pulsepoint.
|
|
67
|
+
*
|
|
68
|
+
* Pulsepoint provides real-time incident data from participating fire departments.
|
|
69
|
+
*
|
|
70
|
+
* @see https://www.pulsepoint.org
|
|
71
|
+
*/
|
|
72
|
+
export class PulsepointPlugin extends BasePlugin {
|
|
73
|
+
metadata = {
|
|
74
|
+
id: 'pulsepoint',
|
|
75
|
+
name: 'Pulsepoint Real-Time Incidents',
|
|
76
|
+
version: '1.0.0',
|
|
77
|
+
description: 'Real-time fire and EMS incidents from Pulsepoint',
|
|
78
|
+
coverage: {
|
|
79
|
+
type: 'regional',
|
|
80
|
+
center: PHOENIX_CENTER,
|
|
81
|
+
radiusMeters: COVERAGE_RADIUS_METERS,
|
|
82
|
+
description: 'Phoenix, AZ metropolitan area (participating agencies)',
|
|
83
|
+
},
|
|
84
|
+
supportedTemporalTypes: ['real-time'],
|
|
85
|
+
supportedCategories: ['fire', 'medical'],
|
|
86
|
+
refreshIntervalMs: 60 * 1000, // 1 minute - real-time data
|
|
87
|
+
};
|
|
88
|
+
pulsepointConfig;
|
|
89
|
+
constructor(config) {
|
|
90
|
+
super(config);
|
|
91
|
+
this.pulsepointConfig = {
|
|
92
|
+
agencyIds: DEFAULT_PHOENIX_AGENCIES,
|
|
93
|
+
includeRecent: false,
|
|
94
|
+
...config,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async fetchAlerts(options) {
|
|
98
|
+
const { location, radiusMeters, categories } = options;
|
|
99
|
+
const cacheKey = this.generateCacheKey(options);
|
|
100
|
+
const warnings = [];
|
|
101
|
+
try {
|
|
102
|
+
const { data, fromCache } = await this.getCachedOrFetch(cacheKey, () => this.fetchAllAgencies(warnings),
|
|
103
|
+
// Short cache TTL for real-time data
|
|
104
|
+
30 * 1000 // 30 seconds
|
|
105
|
+
);
|
|
106
|
+
// Filter by location radius
|
|
107
|
+
let alerts = data.filter((alert) => {
|
|
108
|
+
const distance = this.calculateDistance(location.latitude, location.longitude, alert.location.point.latitude, alert.location.point.longitude);
|
|
109
|
+
return distance <= radiusMeters;
|
|
110
|
+
});
|
|
111
|
+
// Filter by categories if specified
|
|
112
|
+
if (categories && categories.length > 0) {
|
|
113
|
+
alerts = alerts.filter((alert) => categories.includes(alert.category));
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
alerts,
|
|
117
|
+
fromCache,
|
|
118
|
+
cacheKey,
|
|
119
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error('Pulsepoint fetch error:', error);
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Fetch incidents from all configured agencies.
|
|
129
|
+
*/
|
|
130
|
+
async fetchAllAgencies(warnings) {
|
|
131
|
+
const allAlerts = [];
|
|
132
|
+
for (const agencyId of this.pulsepointConfig.agencyIds) {
|
|
133
|
+
try {
|
|
134
|
+
const incidents = await this.fetchAgencyIncidents(agencyId);
|
|
135
|
+
const alerts = incidents.map((incident) => this.transformIncident(incident, agencyId));
|
|
136
|
+
allAlerts.push(...alerts);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
warnings.push(`Failed to fetch from agency ${agencyId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return allAlerts;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Fetch incidents from a single Pulsepoint agency.
|
|
146
|
+
*/
|
|
147
|
+
async fetchAgencyIncidents(agencyId) {
|
|
148
|
+
// Pulsepoint web API endpoint
|
|
149
|
+
const url = `https://web.pulsepoint.org/DB/giba.php?agency_id=${agencyId}`;
|
|
150
|
+
const response = await this.fetchJson(url);
|
|
151
|
+
const incidents = [];
|
|
152
|
+
if (response.incidents?.active) {
|
|
153
|
+
incidents.push(...response.incidents.active);
|
|
154
|
+
}
|
|
155
|
+
if (this.pulsepointConfig.includeRecent && response.incidents?.recent) {
|
|
156
|
+
incidents.push(...response.incidents.recent);
|
|
157
|
+
}
|
|
158
|
+
return incidents;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Transform a Pulsepoint incident to our Alert format.
|
|
162
|
+
*/
|
|
163
|
+
transformIncident(incident, agencyId) {
|
|
164
|
+
const incidentType = incident.IncidentTypeName.toUpperCase();
|
|
165
|
+
const { category, risk } = this.mapIncidentType(incidentType);
|
|
166
|
+
const latitude = parseFloat(incident.Latitude);
|
|
167
|
+
const longitude = parseFloat(incident.Longitude);
|
|
168
|
+
// Use call received time as the incident time
|
|
169
|
+
const issued = incident.CallReceivedDateTime;
|
|
170
|
+
// Build description with unit info
|
|
171
|
+
const description = this.buildDescription(incident);
|
|
172
|
+
return this.createAlert({
|
|
173
|
+
id: `pulsepoint-${agencyId}-${incident.ID}`,
|
|
174
|
+
externalId: incident.ID,
|
|
175
|
+
title: this.formatIncidentType(incident.IncidentTypeName),
|
|
176
|
+
description,
|
|
177
|
+
riskLevel: risk,
|
|
178
|
+
priority: this.riskLevelToPriority(risk),
|
|
179
|
+
category,
|
|
180
|
+
temporalType: 'real-time',
|
|
181
|
+
location: {
|
|
182
|
+
point: { latitude, longitude },
|
|
183
|
+
address: incident.MedicalEmergencyDisplayAddress ?? incident.FullDisplayAddress,
|
|
184
|
+
},
|
|
185
|
+
timestamps: {
|
|
186
|
+
issued,
|
|
187
|
+
eventStart: issued,
|
|
188
|
+
},
|
|
189
|
+
metadata: {
|
|
190
|
+
agencyId,
|
|
191
|
+
incidentTypeCode: incident.IncidentTypeCode,
|
|
192
|
+
units: incident.Unit?.map((u) => ({
|
|
193
|
+
id: u.UnitID,
|
|
194
|
+
status: u.PulsePointDispatchStatus,
|
|
195
|
+
})),
|
|
196
|
+
isClosed: incident.Status?.IncidentClosed ?? false,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Map incident type to category and risk level.
|
|
202
|
+
*/
|
|
203
|
+
mapIncidentType(incidentType) {
|
|
204
|
+
// Try exact match
|
|
205
|
+
if (INCIDENT_TYPE_MAP[incidentType]) {
|
|
206
|
+
return INCIDENT_TYPE_MAP[incidentType];
|
|
207
|
+
}
|
|
208
|
+
// Try partial match
|
|
209
|
+
for (const [key, value] of Object.entries(INCIDENT_TYPE_MAP)) {
|
|
210
|
+
if (incidentType.includes(key)) {
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Default based on keywords
|
|
215
|
+
if (incidentType.includes('FIRE') || incidentType.includes('SMOKE') || incidentType.includes('BURN')) {
|
|
216
|
+
return { category: 'fire', risk: 'high' };
|
|
217
|
+
}
|
|
218
|
+
if (incidentType.includes('MEDICAL') || incidentType.includes('EMS') || incidentType.includes('AMBULANCE')) {
|
|
219
|
+
return { category: 'medical', risk: 'moderate' };
|
|
220
|
+
}
|
|
221
|
+
// Default to medical (most common)
|
|
222
|
+
return { category: 'medical', risk: 'moderate' };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Format incident type for display.
|
|
226
|
+
*/
|
|
227
|
+
formatIncidentType(incidentType) {
|
|
228
|
+
return incidentType
|
|
229
|
+
.toLowerCase()
|
|
230
|
+
.split(' ')
|
|
231
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
232
|
+
.join(' ');
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Build description from incident data.
|
|
236
|
+
*/
|
|
237
|
+
buildDescription(incident) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
parts.push(`Type: ${this.formatIncidentType(incident.IncidentTypeName)}`);
|
|
240
|
+
if (incident.FullDisplayAddress) {
|
|
241
|
+
parts.push(`Location: ${incident.FullDisplayAddress}`);
|
|
242
|
+
}
|
|
243
|
+
if (incident.Unit && incident.Unit.length > 0) {
|
|
244
|
+
const unitList = incident.Unit.map((u) => `${u.UnitID} (${u.PulsePointDispatchStatus})`).join(', ');
|
|
245
|
+
parts.push(`Responding Units: ${unitList}`);
|
|
246
|
+
}
|
|
247
|
+
if (incident.Status?.IncidentClosed) {
|
|
248
|
+
parts.push('Status: Closed');
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
parts.push('Status: Active');
|
|
252
|
+
}
|
|
253
|
+
return parts.join('\n');
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Calculate distance between two points in meters.
|
|
257
|
+
*/
|
|
258
|
+
calculateDistance(lat1, lon1, lat2, lon2) {
|
|
259
|
+
const R = 6371e3; // Earth radius in meters
|
|
260
|
+
const φ1 = (lat1 * Math.PI) / 180;
|
|
261
|
+
const φ2 = (lat2 * Math.PI) / 180;
|
|
262
|
+
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
|
|
263
|
+
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
|
264
|
+
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
|
265
|
+
Math.cos(φ1) * Math.cos(φ2) *
|
|
266
|
+
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
|
267
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
268
|
+
return R * c;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=pulsepoint.plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pulsepoint.plugin.js","sourceRoot":"","sources":["../../../../src/plugins/pulsepoint/pulsepoint.plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { BasePlugin } from '../base-plugin';
|
|
2
|
+
/**
|
|
3
|
+
* Phoenix center coordinates.
|
|
4
|
+
*/
|
|
5
|
+
const PHOENIX_CENTER = {
|
|
6
|
+
latitude: 33.4484,
|
|
7
|
+
longitude: -112.074,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Coverage radius in meters (covers Phoenix metro and major highways).
|
|
11
|
+
*/
|
|
12
|
+
const COVERAGE_RADIUS_METERS = 80_000;
|
|
13
|
+
/**
|
|
14
|
+
* Event type to risk level mapping.
|
|
15
|
+
*/
|
|
16
|
+
const EVENT_TYPE_RISK_MAP = {
|
|
17
|
+
// High severity
|
|
18
|
+
CLOSURE: 'severe',
|
|
19
|
+
CLOSED: 'severe',
|
|
20
|
+
'FULL CLOSURE': 'extreme',
|
|
21
|
+
'MAJOR ACCIDENT': 'severe',
|
|
22
|
+
'MULTI-VEHICLE ACCIDENT': 'severe',
|
|
23
|
+
FATALITY: 'extreme',
|
|
24
|
+
'WRONG WAY DRIVER': 'extreme',
|
|
25
|
+
// Medium severity
|
|
26
|
+
ACCIDENT: 'high',
|
|
27
|
+
CRASH: 'high',
|
|
28
|
+
INCIDENT: 'high',
|
|
29
|
+
'LANE CLOSURE': 'high',
|
|
30
|
+
'LANES BLOCKED': 'high',
|
|
31
|
+
DEBRIS: 'moderate',
|
|
32
|
+
'DISABLED VEHICLE': 'moderate',
|
|
33
|
+
CONSTRUCTION: 'moderate',
|
|
34
|
+
ROADWORK: 'moderate',
|
|
35
|
+
// Lower severity
|
|
36
|
+
CONGESTION: 'moderate',
|
|
37
|
+
'SLOW TRAFFIC': 'low',
|
|
38
|
+
EVENT: 'low',
|
|
39
|
+
'SPECIAL EVENT': 'moderate',
|
|
40
|
+
WEATHER: 'high',
|
|
41
|
+
'DUST STORM': 'severe',
|
|
42
|
+
FLOODING: 'severe',
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Plugin that fetches traffic incident data for Arizona.
|
|
46
|
+
*
|
|
47
|
+
* Uses ADOT (Arizona DOT) data feeds for traffic incidents,
|
|
48
|
+
* road closures, construction, and congestion information.
|
|
49
|
+
*
|
|
50
|
+
* @see https://az511.gov
|
|
51
|
+
*/
|
|
52
|
+
export class ArizonaTrafficPlugin extends BasePlugin {
|
|
53
|
+
metadata = {
|
|
54
|
+
id: 'arizona-traffic',
|
|
55
|
+
name: 'Arizona Traffic',
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
description: 'Traffic incidents, road closures, and construction for Arizona',
|
|
58
|
+
coverage: {
|
|
59
|
+
type: 'regional',
|
|
60
|
+
center: PHOENIX_CENTER,
|
|
61
|
+
radiusMeters: COVERAGE_RADIUS_METERS,
|
|
62
|
+
description: 'Phoenix metropolitan area and major Arizona highways',
|
|
63
|
+
},
|
|
64
|
+
supportedTemporalTypes: ['real-time', 'scheduled'],
|
|
65
|
+
supportedCategories: ['traffic'],
|
|
66
|
+
refreshIntervalMs: 5 * 60 * 1000, // 5 minutes
|
|
67
|
+
};
|
|
68
|
+
trafficConfig;
|
|
69
|
+
constructor(config) {
|
|
70
|
+
super(config);
|
|
71
|
+
this.trafficConfig = {
|
|
72
|
+
includeConstruction: true,
|
|
73
|
+
closuresOnly: false,
|
|
74
|
+
minDelayMinutes: 0,
|
|
75
|
+
...config,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async fetchAlerts(options) {
|
|
79
|
+
const { location, radiusMeters } = options;
|
|
80
|
+
const cacheKey = this.generateCacheKey(options);
|
|
81
|
+
const warnings = [];
|
|
82
|
+
try {
|
|
83
|
+
const { data, fromCache } = await this.getCachedOrFetch(cacheKey, () => this.fetchTrafficEvents(location, radiusMeters, warnings), this.config.cacheTtlMs);
|
|
84
|
+
return {
|
|
85
|
+
alerts: data,
|
|
86
|
+
fromCache,
|
|
87
|
+
cacheKey,
|
|
88
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error('Arizona Traffic fetch error:', error);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fetch traffic events from ADOT sources.
|
|
98
|
+
*/
|
|
99
|
+
async fetchTrafficEvents(location, radiusMeters, warnings) {
|
|
100
|
+
const allAlerts = [];
|
|
101
|
+
// Fetch from multiple ADOT endpoints
|
|
102
|
+
const sources = [
|
|
103
|
+
{ url: this.buildIncidentsUrl(location), type: 'incidents' },
|
|
104
|
+
{ url: this.buildConstructionUrl(location), type: 'construction' },
|
|
105
|
+
{ url: this.buildClosuresUrl(location), type: 'closures' },
|
|
106
|
+
];
|
|
107
|
+
for (const source of sources) {
|
|
108
|
+
if (!this.trafficConfig.includeConstruction && source.type === 'construction') {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const events = await this.fetchFromSource(source.url, source.type);
|
|
113
|
+
const filtered = events.filter((event) => {
|
|
114
|
+
// Filter by location
|
|
115
|
+
const distance = this.calculateDistance(location.latitude, location.longitude, event.latitude, event.longitude);
|
|
116
|
+
if (distance > radiusMeters)
|
|
117
|
+
return false;
|
|
118
|
+
// Filter by closures only if configured
|
|
119
|
+
if (this.trafficConfig.closuresOnly) {
|
|
120
|
+
const eventType = (event.event_type ?? '').toUpperCase();
|
|
121
|
+
if (!eventType.includes('CLOSURE') && !eventType.includes('CLOSED')) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Filter by minimum delay
|
|
126
|
+
if (this.trafficConfig.minDelayMinutes && event.delay_minutes) {
|
|
127
|
+
if (event.delay_minutes < this.trafficConfig.minDelayMinutes) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
});
|
|
133
|
+
const alerts = filtered.map((event) => this.transformEvent(event));
|
|
134
|
+
allAlerts.push(...alerts);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
warnings.push(`Failed to fetch ${source.type}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Deduplicate by ID
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
return allAlerts.filter((alert) => {
|
|
143
|
+
if (seen.has(alert.id))
|
|
144
|
+
return false;
|
|
145
|
+
seen.add(alert.id);
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Build URL for traffic incidents.
|
|
151
|
+
* Note: Location filtering is done client-side after fetching all Arizona data.
|
|
152
|
+
*/
|
|
153
|
+
buildIncidentsUrl(_location) {
|
|
154
|
+
// ADOT Open Data Portal - Traffic Events
|
|
155
|
+
// Note: This is a public GeoJSON endpoint
|
|
156
|
+
return 'https://services1.arcgis.com/0MSEUqKaxRlEPj5g/arcgis/rest/services/ADOT_Traffic_Events/FeatureServer/0/query?where=1%3D1&outFields=*&f=geojson';
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Build URL for construction events.
|
|
160
|
+
* Note: Location filtering is done client-side after fetching all Arizona data.
|
|
161
|
+
*/
|
|
162
|
+
buildConstructionUrl(_location) {
|
|
163
|
+
return 'https://services1.arcgis.com/0MSEUqKaxRlEPj5g/arcgis/rest/services/ADOT_Construction_Projects/FeatureServer/0/query?where=1%3D1&outFields=*&f=geojson';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build URL for road closures.
|
|
167
|
+
* Note: Location filtering is done client-side after fetching all Arizona data.
|
|
168
|
+
*/
|
|
169
|
+
buildClosuresUrl(_location) {
|
|
170
|
+
return 'https://services1.arcgis.com/0MSEUqKaxRlEPj5g/arcgis/rest/services/ADOT_Road_Closures/FeatureServer/0/query?where=1%3D1&outFields=*&f=geojson';
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Fetch from a specific source and normalize the data.
|
|
174
|
+
*/
|
|
175
|
+
async fetchFromSource(url, type) {
|
|
176
|
+
try {
|
|
177
|
+
const response = await this.fetchJson(url);
|
|
178
|
+
if (!response.features) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
return response.features
|
|
182
|
+
.filter((feature) => feature.geometry)
|
|
183
|
+
.map((feature) => this.normalizeFeature(feature, type));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
// If the ADOT endpoint fails, try a fallback approach
|
|
187
|
+
console.warn(`ADOT ${type} endpoint failed, returning empty:`, error);
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Normalize a GeoJSON feature to our traffic event structure.
|
|
193
|
+
*/
|
|
194
|
+
normalizeFeature(feature, sourceType) {
|
|
195
|
+
const props = feature.properties;
|
|
196
|
+
// Extract coordinates based on geometry type
|
|
197
|
+
let latitude = 0;
|
|
198
|
+
let longitude = 0;
|
|
199
|
+
if (feature.geometry.type === 'Point') {
|
|
200
|
+
const coords = feature.geometry.coordinates;
|
|
201
|
+
longitude = coords[0];
|
|
202
|
+
latitude = coords[1];
|
|
203
|
+
}
|
|
204
|
+
else if (feature.geometry.type === 'LineString' || feature.geometry.type === 'MultiPoint') {
|
|
205
|
+
// Use first coordinate
|
|
206
|
+
const coords = feature.geometry.coordinates;
|
|
207
|
+
if (coords.length > 0) {
|
|
208
|
+
longitude = coords[0][0];
|
|
209
|
+
latitude = coords[0][1];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
id: feature.id?.toString() ?? String(props.OBJECTID ?? props.id ?? Date.now()),
|
|
214
|
+
event_type: String(props.EVENT_TYPE ?? props.Type ?? props.event_type ?? sourceType).toUpperCase(),
|
|
215
|
+
event_subtype: props.EVENT_SUBTYPE,
|
|
216
|
+
severity: props.SEVERITY,
|
|
217
|
+
headline: (props.HEADLINE ?? props.Title ?? props.headline),
|
|
218
|
+
description: (props.DESCRIPTION ?? props.Description ?? props.description),
|
|
219
|
+
road_name: (props.ROAD_NAME ?? props.Route ?? props.road_name),
|
|
220
|
+
direction: props.DIRECTION,
|
|
221
|
+
from_location: props.FROM_LOCATION,
|
|
222
|
+
to_location: props.TO_LOCATION,
|
|
223
|
+
latitude,
|
|
224
|
+
longitude,
|
|
225
|
+
start_time: (props.START_TIME ?? props.StartDate ?? props.start_time),
|
|
226
|
+
end_time: (props.END_TIME ?? props.EndDate ?? props.end_time),
|
|
227
|
+
last_updated: (props.LAST_UPDATED ?? props.LastUpdated ?? props.last_updated),
|
|
228
|
+
lanes_affected: props.LANES_AFFECTED,
|
|
229
|
+
lanes_blocked: props.LANES_BLOCKED,
|
|
230
|
+
delay_minutes: props.DELAY_MINUTES,
|
|
231
|
+
is_major: Boolean(props.IS_MAJOR ?? props.Major),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Transform a traffic event to our Alert format.
|
|
236
|
+
*/
|
|
237
|
+
transformEvent(event) {
|
|
238
|
+
const riskLevel = this.mapEventTypeToRisk(event.event_type, event.severity, event.is_major);
|
|
239
|
+
const temporalType = event.start_time && new Date(event.start_time) > new Date() ? 'scheduled' : 'real-time';
|
|
240
|
+
return this.createAlert({
|
|
241
|
+
id: `az-traffic-${event.id}`,
|
|
242
|
+
externalId: event.id,
|
|
243
|
+
title: this.buildTitle(event),
|
|
244
|
+
description: this.buildDescription(event),
|
|
245
|
+
riskLevel,
|
|
246
|
+
priority: this.riskLevelToPriority(riskLevel),
|
|
247
|
+
category: 'traffic',
|
|
248
|
+
temporalType,
|
|
249
|
+
location: {
|
|
250
|
+
point: { latitude: event.latitude, longitude: event.longitude },
|
|
251
|
+
address: this.buildAddress(event),
|
|
252
|
+
},
|
|
253
|
+
timestamps: {
|
|
254
|
+
issued: event.last_updated ?? new Date().toISOString(),
|
|
255
|
+
eventStart: event.start_time,
|
|
256
|
+
eventEnd: event.end_time,
|
|
257
|
+
},
|
|
258
|
+
metadata: {
|
|
259
|
+
eventType: event.event_type,
|
|
260
|
+
eventSubtype: event.event_subtype,
|
|
261
|
+
roadName: event.road_name,
|
|
262
|
+
direction: event.direction,
|
|
263
|
+
lanesAffected: event.lanes_affected,
|
|
264
|
+
lanesBlocked: event.lanes_blocked,
|
|
265
|
+
delayMinutes: event.delay_minutes,
|
|
266
|
+
isMajor: event.is_major,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Map event type to risk level.
|
|
272
|
+
*/
|
|
273
|
+
mapEventTypeToRisk(eventType, severity, isMajor) {
|
|
274
|
+
// Check for major events first
|
|
275
|
+
if (isMajor) {
|
|
276
|
+
return 'severe';
|
|
277
|
+
}
|
|
278
|
+
// Check severity if provided
|
|
279
|
+
if (severity) {
|
|
280
|
+
const sev = severity.toUpperCase();
|
|
281
|
+
if (sev.includes('CRITICAL') || sev.includes('EXTREME'))
|
|
282
|
+
return 'extreme';
|
|
283
|
+
if (sev.includes('MAJOR') || sev.includes('SEVERE'))
|
|
284
|
+
return 'severe';
|
|
285
|
+
if (sev.includes('MODERATE') || sev.includes('SIGNIFICANT'))
|
|
286
|
+
return 'high';
|
|
287
|
+
if (sev.includes('MINOR'))
|
|
288
|
+
return 'moderate';
|
|
289
|
+
}
|
|
290
|
+
// Check event type mapping
|
|
291
|
+
const normalizedType = eventType.toUpperCase();
|
|
292
|
+
if (EVENT_TYPE_RISK_MAP[normalizedType]) {
|
|
293
|
+
return EVENT_TYPE_RISK_MAP[normalizedType];
|
|
294
|
+
}
|
|
295
|
+
// Partial match
|
|
296
|
+
for (const [key, risk] of Object.entries(EVENT_TYPE_RISK_MAP)) {
|
|
297
|
+
if (normalizedType.includes(key)) {
|
|
298
|
+
return risk;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return 'moderate';
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Build title from event data.
|
|
305
|
+
*/
|
|
306
|
+
buildTitle(event) {
|
|
307
|
+
if (event.headline) {
|
|
308
|
+
return event.headline;
|
|
309
|
+
}
|
|
310
|
+
const parts = [];
|
|
311
|
+
// Event type
|
|
312
|
+
const eventType = this.formatEventType(event.event_type);
|
|
313
|
+
parts.push(eventType);
|
|
314
|
+
// Road name
|
|
315
|
+
if (event.road_name) {
|
|
316
|
+
parts.push(`on ${event.road_name}`);
|
|
317
|
+
}
|
|
318
|
+
// Direction
|
|
319
|
+
if (event.direction) {
|
|
320
|
+
parts.push(event.direction);
|
|
321
|
+
}
|
|
322
|
+
return parts.join(' ');
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Format event type for display.
|
|
326
|
+
*/
|
|
327
|
+
formatEventType(eventType) {
|
|
328
|
+
return eventType
|
|
329
|
+
.toLowerCase()
|
|
330
|
+
.replace(/_/g, ' ')
|
|
331
|
+
.split(' ')
|
|
332
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
333
|
+
.join(' ');
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Build description from event data.
|
|
337
|
+
*/
|
|
338
|
+
buildDescription(event) {
|
|
339
|
+
const parts = [];
|
|
340
|
+
if (event.description) {
|
|
341
|
+
parts.push(event.description);
|
|
342
|
+
}
|
|
343
|
+
if (event.lanes_affected || event.lanes_blocked) {
|
|
344
|
+
const lanes = event.lanes_blocked ?? event.lanes_affected;
|
|
345
|
+
parts.push(`Lanes affected: ${lanes}`);
|
|
346
|
+
}
|
|
347
|
+
if (event.delay_minutes) {
|
|
348
|
+
parts.push(`Estimated delay: ${event.delay_minutes} minutes`);
|
|
349
|
+
}
|
|
350
|
+
if (event.from_location && event.to_location) {
|
|
351
|
+
parts.push(`From: ${event.from_location} To: ${event.to_location}`);
|
|
352
|
+
}
|
|
353
|
+
return parts.join('\n') || this.buildTitle(event);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Build address from event data.
|
|
357
|
+
*/
|
|
358
|
+
buildAddress(event) {
|
|
359
|
+
const parts = [];
|
|
360
|
+
if (event.road_name) {
|
|
361
|
+
parts.push(event.road_name);
|
|
362
|
+
}
|
|
363
|
+
if (event.direction) {
|
|
364
|
+
parts.push(event.direction);
|
|
365
|
+
}
|
|
366
|
+
if (event.from_location) {
|
|
367
|
+
parts.push(`near ${event.from_location}`);
|
|
368
|
+
}
|
|
369
|
+
return parts.join(' ') || 'Arizona';
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Calculate distance between two points in meters.
|
|
373
|
+
*/
|
|
374
|
+
calculateDistance(lat1, lon1, lat2, lon2) {
|
|
375
|
+
const R = 6371e3;
|
|
376
|
+
const φ1 = (lat1 * Math.PI) / 180;
|
|
377
|
+
const φ2 = (lat2 * Math.PI) / 180;
|
|
378
|
+
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
|
|
379
|
+
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
|
380
|
+
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
|
381
|
+
Math.cos(φ1) * Math.cos(φ2) *
|
|
382
|
+
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
|
383
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
384
|
+
return R * c;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
//# sourceMappingURL=arizona-traffic.plugin.js.map
|