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.
Files changed (283) hide show
  1. package/dist/cjs/adapters/index.js +8 -0
  2. package/dist/cjs/adapters/index.js.map +1 -0
  3. package/dist/cjs/adapters/lambda.js +143 -0
  4. package/dist/cjs/adapters/lambda.js.map +1 -0
  5. package/dist/cjs/adapters/vercel.js +119 -0
  6. package/dist/cjs/adapters/vercel.js.map +1 -0
  7. package/dist/cjs/core/alert-aggregator.js +207 -0
  8. package/dist/cjs/core/alert-aggregator.js.map +1 -0
  9. package/dist/cjs/core/alert-feed.js +236 -0
  10. package/dist/cjs/core/alert-feed.js.map +1 -0
  11. package/dist/cjs/core/index.js +22 -0
  12. package/dist/cjs/core/index.js.map +1 -0
  13. package/dist/cjs/core/plugin-registry.js +193 -0
  14. package/dist/cjs/core/plugin-registry.js.map +1 -0
  15. package/dist/cjs/core/plugin-resolver.js +121 -0
  16. package/dist/cjs/core/plugin-resolver.js.map +1 -0
  17. package/dist/cjs/core/time-range.js +67 -0
  18. package/dist/cjs/core/time-range.js.map +1 -0
  19. package/dist/cjs/errors/fetch-error.js +71 -0
  20. package/dist/cjs/errors/fetch-error.js.map +1 -0
  21. package/dist/cjs/errors/index.js +15 -0
  22. package/dist/cjs/errors/index.js.map +1 -0
  23. package/dist/cjs/errors/plugin-error.js +80 -0
  24. package/dist/cjs/errors/plugin-error.js.map +1 -0
  25. package/dist/cjs/errors/validation-error.js +49 -0
  26. package/dist/cjs/errors/validation-error.js.map +1 -0
  27. package/dist/cjs/geo/distance.js +94 -0
  28. package/dist/cjs/geo/distance.js.map +1 -0
  29. package/dist/cjs/geo/index.js +18 -0
  30. package/dist/cjs/geo/index.js.map +1 -0
  31. package/dist/cjs/geo/point-in-radius.js +86 -0
  32. package/dist/cjs/geo/point-in-radius.js.map +1 -0
  33. package/dist/cjs/index.js +90 -0
  34. package/dist/cjs/index.js.map +1 -0
  35. package/dist/cjs/plugins/air-quality/airnow.plugin.js +343 -0
  36. package/dist/cjs/plugins/air-quality/airnow.plugin.js.map +1 -0
  37. package/dist/cjs/plugins/air-quality/index.js +6 -0
  38. package/dist/cjs/plugins/air-quality/index.js.map +1 -0
  39. package/dist/cjs/plugins/base-plugin.js +213 -0
  40. package/dist/cjs/plugins/base-plugin.js.map +1 -0
  41. package/dist/cjs/plugins/events/index.js +6 -0
  42. package/dist/cjs/plugins/events/index.js.map +1 -0
  43. package/dist/cjs/plugins/events/phoenix-events.plugin.js +382 -0
  44. package/dist/cjs/plugins/events/phoenix-events.plugin.js.map +1 -0
  45. package/dist/cjs/plugins/fire-emt/index.js +6 -0
  46. package/dist/cjs/plugins/fire-emt/index.js.map +1 -0
  47. package/dist/cjs/plugins/fire-emt/phoenix-fire.plugin.js +262 -0
  48. package/dist/cjs/plugins/fire-emt/phoenix-fire.plugin.js.map +1 -0
  49. package/dist/cjs/plugins/index.js +28 -0
  50. package/dist/cjs/plugins/index.js.map +1 -0
  51. package/dist/cjs/plugins/police-blotter/index.js +6 -0
  52. package/dist/cjs/plugins/police-blotter/index.js.map +1 -0
  53. package/dist/cjs/plugins/police-blotter/phoenix-police.plugin.js +198 -0
  54. package/dist/cjs/plugins/police-blotter/phoenix-police.plugin.js.map +1 -0
  55. package/dist/cjs/plugins/pulsepoint/index.js +6 -0
  56. package/dist/cjs/plugins/pulsepoint/index.js.map +1 -0
  57. package/dist/cjs/plugins/pulsepoint/pulsepoint.plugin.js +275 -0
  58. package/dist/cjs/plugins/pulsepoint/pulsepoint.plugin.js.map +1 -0
  59. package/dist/cjs/plugins/traffic/arizona-traffic.plugin.js +391 -0
  60. package/dist/cjs/plugins/traffic/arizona-traffic.plugin.js.map +1 -0
  61. package/dist/cjs/plugins/traffic/index.js +6 -0
  62. package/dist/cjs/plugins/traffic/index.js.map +1 -0
  63. package/dist/cjs/plugins/weather/index.js +6 -0
  64. package/dist/cjs/plugins/weather/index.js.map +1 -0
  65. package/dist/cjs/plugins/weather/nws-weather.plugin.js +180 -0
  66. package/dist/cjs/plugins/weather/nws-weather.plugin.js.map +1 -0
  67. package/dist/cjs/schemas/alert.schema.js +93 -0
  68. package/dist/cjs/schemas/alert.schema.js.map +1 -0
  69. package/dist/cjs/schemas/index.js +24 -0
  70. package/dist/cjs/schemas/index.js.map +1 -0
  71. package/dist/cjs/schemas/query.schema.js +76 -0
  72. package/dist/cjs/schemas/query.schema.js.map +1 -0
  73. package/dist/cjs/types/alert.js +35 -0
  74. package/dist/cjs/types/alert.js.map +1 -0
  75. package/dist/cjs/types/config.js +13 -0
  76. package/dist/cjs/types/config.js.map +1 -0
  77. package/dist/cjs/types/geo.js +3 -0
  78. package/dist/cjs/types/geo.js.map +1 -0
  79. package/dist/cjs/types/index.js +16 -0
  80. package/dist/cjs/types/index.js.map +1 -0
  81. package/dist/cjs/types/plugin.js +3 -0
  82. package/dist/cjs/types/plugin.js.map +1 -0
  83. package/dist/cjs/types/query.js +28 -0
  84. package/dist/cjs/types/query.js.map +1 -0
  85. package/dist/cjs/utils/cache.js +188 -0
  86. package/dist/cjs/utils/cache.js.map +1 -0
  87. package/dist/cjs/utils/csv.js +189 -0
  88. package/dist/cjs/utils/csv.js.map +1 -0
  89. package/dist/cjs/utils/date.js +153 -0
  90. package/dist/cjs/utils/date.js.map +1 -0
  91. package/dist/cjs/utils/index.js +28 -0
  92. package/dist/cjs/utils/index.js.map +1 -0
  93. package/dist/cjs/utils/retry.js +109 -0
  94. package/dist/cjs/utils/retry.js.map +1 -0
  95. package/dist/esm/adapters/index.js +3 -0
  96. package/dist/esm/adapters/index.js.map +1 -0
  97. package/dist/esm/adapters/lambda.js +140 -0
  98. package/dist/esm/adapters/lambda.js.map +1 -0
  99. package/dist/esm/adapters/vercel.js +116 -0
  100. package/dist/esm/adapters/vercel.js.map +1 -0
  101. package/dist/esm/core/alert-aggregator.js +203 -0
  102. package/dist/esm/core/alert-aggregator.js.map +1 -0
  103. package/dist/esm/core/alert-feed.js +232 -0
  104. package/dist/esm/core/alert-feed.js.map +1 -0
  105. package/dist/esm/core/index.js +6 -0
  106. package/dist/esm/core/index.js.map +1 -0
  107. package/dist/esm/core/plugin-registry.js +189 -0
  108. package/dist/esm/core/plugin-registry.js.map +1 -0
  109. package/dist/esm/core/plugin-resolver.js +117 -0
  110. package/dist/esm/core/plugin-resolver.js.map +1 -0
  111. package/dist/esm/core/time-range.js +57 -0
  112. package/dist/esm/core/time-range.js.map +1 -0
  113. package/dist/esm/errors/fetch-error.js +67 -0
  114. package/dist/esm/errors/fetch-error.js.map +1 -0
  115. package/dist/esm/errors/index.js +4 -0
  116. package/dist/esm/errors/index.js.map +1 -0
  117. package/dist/esm/errors/plugin-error.js +71 -0
  118. package/dist/esm/errors/plugin-error.js.map +1 -0
  119. package/dist/esm/errors/validation-error.js +45 -0
  120. package/dist/esm/errors/validation-error.js.map +1 -0
  121. package/dist/esm/geo/distance.js +85 -0
  122. package/dist/esm/geo/distance.js.map +1 -0
  123. package/dist/esm/geo/index.js +3 -0
  124. package/dist/esm/geo/index.js.map +1 -0
  125. package/dist/esm/geo/point-in-radius.js +79 -0
  126. package/dist/esm/geo/point-in-radius.js.map +1 -0
  127. package/dist/esm/index.js +30 -0
  128. package/dist/esm/index.js.map +1 -0
  129. package/dist/esm/plugins/air-quality/airnow.plugin.js +339 -0
  130. package/dist/esm/plugins/air-quality/airnow.plugin.js.map +1 -0
  131. package/dist/esm/plugins/air-quality/index.js +2 -0
  132. package/dist/esm/plugins/air-quality/index.js.map +1 -0
  133. package/dist/esm/plugins/base-plugin.js +209 -0
  134. package/dist/esm/plugins/base-plugin.js.map +1 -0
  135. package/dist/esm/plugins/events/index.js +2 -0
  136. package/dist/esm/plugins/events/index.js.map +1 -0
  137. package/dist/esm/plugins/events/phoenix-events.plugin.js +378 -0
  138. package/dist/esm/plugins/events/phoenix-events.plugin.js.map +1 -0
  139. package/dist/esm/plugins/fire-emt/index.js +2 -0
  140. package/dist/esm/plugins/fire-emt/index.js.map +1 -0
  141. package/dist/esm/plugins/fire-emt/phoenix-fire.plugin.js +258 -0
  142. package/dist/esm/plugins/fire-emt/phoenix-fire.plugin.js.map +1 -0
  143. package/dist/esm/plugins/index.js +17 -0
  144. package/dist/esm/plugins/index.js.map +1 -0
  145. package/dist/esm/plugins/police-blotter/index.js +2 -0
  146. package/dist/esm/plugins/police-blotter/index.js.map +1 -0
  147. package/dist/esm/plugins/police-blotter/phoenix-police.plugin.js +194 -0
  148. package/dist/esm/plugins/police-blotter/phoenix-police.plugin.js.map +1 -0
  149. package/dist/esm/plugins/pulsepoint/index.js +2 -0
  150. package/dist/esm/plugins/pulsepoint/index.js.map +1 -0
  151. package/dist/esm/plugins/pulsepoint/pulsepoint.plugin.js +271 -0
  152. package/dist/esm/plugins/pulsepoint/pulsepoint.plugin.js.map +1 -0
  153. package/dist/esm/plugins/traffic/arizona-traffic.plugin.js +387 -0
  154. package/dist/esm/plugins/traffic/arizona-traffic.plugin.js.map +1 -0
  155. package/dist/esm/plugins/traffic/index.js +2 -0
  156. package/dist/esm/plugins/traffic/index.js.map +1 -0
  157. package/dist/esm/plugins/weather/index.js +2 -0
  158. package/dist/esm/plugins/weather/index.js.map +1 -0
  159. package/dist/esm/plugins/weather/nws-weather.plugin.js +176 -0
  160. package/dist/esm/plugins/weather/nws-weather.plugin.js.map +1 -0
  161. package/dist/esm/schemas/alert.schema.js +90 -0
  162. package/dist/esm/schemas/alert.schema.js.map +1 -0
  163. package/dist/esm/schemas/index.js +5 -0
  164. package/dist/esm/schemas/index.js.map +1 -0
  165. package/dist/esm/schemas/query.schema.js +72 -0
  166. package/dist/esm/schemas/query.schema.js.map +1 -0
  167. package/dist/esm/types/alert.js +32 -0
  168. package/dist/esm/types/alert.js.map +1 -0
  169. package/dist/esm/types/config.js +10 -0
  170. package/dist/esm/types/config.js.map +1 -0
  171. package/dist/esm/types/geo.js +2 -0
  172. package/dist/esm/types/geo.js.map +1 -0
  173. package/dist/esm/types/index.js +4 -0
  174. package/dist/esm/types/index.js.map +1 -0
  175. package/dist/esm/types/plugin.js +2 -0
  176. package/dist/esm/types/plugin.js.map +1 -0
  177. package/dist/esm/types/query.js +25 -0
  178. package/dist/esm/types/query.js.map +1 -0
  179. package/dist/esm/utils/cache.js +181 -0
  180. package/dist/esm/utils/cache.js.map +1 -0
  181. package/dist/esm/utils/csv.js +185 -0
  182. package/dist/esm/utils/csv.js.map +1 -0
  183. package/dist/esm/utils/date.js +142 -0
  184. package/dist/esm/utils/date.js.map +1 -0
  185. package/dist/esm/utils/index.js +5 -0
  186. package/dist/esm/utils/index.js.map +1 -0
  187. package/dist/esm/utils/retry.js +102 -0
  188. package/dist/esm/utils/retry.js.map +1 -0
  189. package/dist/types/adapters/index.d.ts +5 -0
  190. package/dist/types/adapters/index.d.ts.map +1 -0
  191. package/dist/types/adapters/lambda.d.ts +37 -0
  192. package/dist/types/adapters/lambda.d.ts.map +1 -0
  193. package/dist/types/adapters/vercel.d.ts +54 -0
  194. package/dist/types/adapters/vercel.d.ts.map +1 -0
  195. package/dist/types/core/alert-aggregator.d.ts +81 -0
  196. package/dist/types/core/alert-aggregator.d.ts.map +1 -0
  197. package/dist/types/core/alert-feed.d.ts +80 -0
  198. package/dist/types/core/alert-feed.d.ts.map +1 -0
  199. package/dist/types/core/index.d.ts +8 -0
  200. package/dist/types/core/index.d.ts.map +1 -0
  201. package/dist/types/core/plugin-registry.d.ts +91 -0
  202. package/dist/types/core/plugin-registry.d.ts.map +1 -0
  203. package/dist/types/core/plugin-resolver.d.ts +78 -0
  204. package/dist/types/core/plugin-resolver.d.ts.map +1 -0
  205. package/dist/types/core/time-range.d.ts +40 -0
  206. package/dist/types/core/time-range.d.ts.map +1 -0
  207. package/dist/types/errors/fetch-error.d.ts +46 -0
  208. package/dist/types/errors/fetch-error.d.ts.map +1 -0
  209. package/dist/types/errors/index.d.ts +5 -0
  210. package/dist/types/errors/index.d.ts.map +1 -0
  211. package/dist/types/errors/plugin-error.d.ts +42 -0
  212. package/dist/types/errors/plugin-error.d.ts.map +1 -0
  213. package/dist/types/errors/validation-error.d.ts +34 -0
  214. package/dist/types/errors/validation-error.d.ts.map +1 -0
  215. package/dist/types/geo/distance.d.ts +50 -0
  216. package/dist/types/geo/distance.d.ts.map +1 -0
  217. package/dist/types/geo/index.d.ts +3 -0
  218. package/dist/types/geo/index.d.ts.map +1 -0
  219. package/dist/types/geo/point-in-radius.d.ts +44 -0
  220. package/dist/types/geo/point-in-radius.d.ts.map +1 -0
  221. package/dist/types/index.d.ts +32 -0
  222. package/dist/types/index.d.ts.map +1 -0
  223. package/dist/types/plugins/air-quality/airnow.plugin.d.ts +84 -0
  224. package/dist/types/plugins/air-quality/airnow.plugin.d.ts.map +1 -0
  225. package/dist/types/plugins/air-quality/index.d.ts +3 -0
  226. package/dist/types/plugins/air-quality/index.d.ts.map +1 -0
  227. package/dist/types/plugins/base-plugin.d.ts +99 -0
  228. package/dist/types/plugins/base-plugin.d.ts.map +1 -0
  229. package/dist/types/plugins/events/index.d.ts +3 -0
  230. package/dist/types/plugins/events/index.d.ts.map +1 -0
  231. package/dist/types/plugins/events/phoenix-events.plugin.d.ts +71 -0
  232. package/dist/types/plugins/events/phoenix-events.plugin.d.ts.map +1 -0
  233. package/dist/types/plugins/fire-emt/index.d.ts +3 -0
  234. package/dist/types/plugins/fire-emt/index.d.ts.map +1 -0
  235. package/dist/types/plugins/fire-emt/phoenix-fire.plugin.d.ts +47 -0
  236. package/dist/types/plugins/fire-emt/phoenix-fire.plugin.d.ts.map +1 -0
  237. package/dist/types/plugins/index.d.ts +17 -0
  238. package/dist/types/plugins/index.d.ts.map +1 -0
  239. package/dist/types/plugins/police-blotter/index.d.ts +3 -0
  240. package/dist/types/plugins/police-blotter/index.d.ts.map +1 -0
  241. package/dist/types/plugins/police-blotter/phoenix-police.plugin.d.ts +49 -0
  242. package/dist/types/plugins/police-blotter/phoenix-police.plugin.d.ts.map +1 -0
  243. package/dist/types/plugins/pulsepoint/index.d.ts +3 -0
  244. package/dist/types/plugins/pulsepoint/index.d.ts.map +1 -0
  245. package/dist/types/plugins/pulsepoint/pulsepoint.plugin.d.ts +61 -0
  246. package/dist/types/plugins/pulsepoint/pulsepoint.plugin.d.ts.map +1 -0
  247. package/dist/types/plugins/traffic/arizona-traffic.plugin.d.ts +83 -0
  248. package/dist/types/plugins/traffic/arizona-traffic.plugin.d.ts.map +1 -0
  249. package/dist/types/plugins/traffic/index.d.ts +3 -0
  250. package/dist/types/plugins/traffic/index.d.ts.map +1 -0
  251. package/dist/types/plugins/weather/index.d.ts +3 -0
  252. package/dist/types/plugins/weather/index.d.ts.map +1 -0
  253. package/dist/types/plugins/weather/nws-weather.plugin.d.ts +50 -0
  254. package/dist/types/plugins/weather/nws-weather.plugin.d.ts.map +1 -0
  255. package/dist/types/schemas/alert.schema.d.ts +266 -0
  256. package/dist/types/schemas/alert.schema.d.ts.map +1 -0
  257. package/dist/types/schemas/index.d.ts +5 -0
  258. package/dist/types/schemas/index.d.ts.map +1 -0
  259. package/dist/types/schemas/query.schema.d.ts +150 -0
  260. package/dist/types/schemas/query.schema.d.ts.map +1 -0
  261. package/dist/types/types/alert.d.ts +96 -0
  262. package/dist/types/types/alert.d.ts.map +1 -0
  263. package/dist/types/types/config.d.ts +63 -0
  264. package/dist/types/types/config.d.ts.map +1 -0
  265. package/dist/types/types/geo.d.ts +33 -0
  266. package/dist/types/types/geo.d.ts.map +1 -0
  267. package/dist/types/types/index.d.ts +9 -0
  268. package/dist/types/types/index.d.ts.map +1 -0
  269. package/dist/types/types/plugin.d.ts +125 -0
  270. package/dist/types/types/plugin.d.ts.map +1 -0
  271. package/dist/types/types/query.d.ts +86 -0
  272. package/dist/types/types/query.d.ts.map +1 -0
  273. package/dist/types/utils/cache.d.ts +112 -0
  274. package/dist/types/utils/cache.d.ts.map +1 -0
  275. package/dist/types/utils/csv.d.ts +38 -0
  276. package/dist/types/utils/csv.d.ts.map +1 -0
  277. package/dist/types/utils/date.d.ts +47 -0
  278. package/dist/types/utils/date.d.ts.map +1 -0
  279. package/dist/types/utils/index.d.ts +7 -0
  280. package/dist/types/utils/index.d.ts.map +1 -0
  281. package/dist/types/utils/retry.d.ts +51 -0
  282. package/dist/types/utils/retry.d.ts.map +1 -0
  283. 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